mirror of
https://github.com/beetbox/beets.git
synced 2025-12-17 14:13:41 +01:00
backport YAML dumping from Confit (#552)
We actually added more full-blown YAML dumping to the Confit library a while back but it looks like it never made it into beets. It offers a few benefits over the hand-rolled flattening that the `config` command was previously using, including printing ordered dicts in the right order. But it also appears to have broken logic when attempting to hide defaults. I'll fix this right quick.
This commit is contained in:
parent
692645466e
commit
3b2d51e018
2 changed files with 167 additions and 14 deletions
|
|
@ -1250,6 +1250,8 @@ write_cmd.func = write_func
|
|||
default_commands.append(write_cmd)
|
||||
|
||||
|
||||
# config: Show and edit user configuration.
|
||||
|
||||
config_cmd = ui.Subcommand('config', help='show or edit the user configuration')
|
||||
config_cmd.parser.add_option('-p', '--paths', action='store_true',
|
||||
help='show files that configuration was loaded from')
|
||||
|
|
@ -1257,25 +1259,19 @@ config_cmd.parser.add_option('-e', '--edit', action='store_true',
|
|||
help='edit user configuration with $EDITOR')
|
||||
config_cmd.parser.add_option('-d', '--defaults', action='store_true',
|
||||
help='include the default configuration')
|
||||
def _config_get(view):
|
||||
try:
|
||||
keys = view.keys()
|
||||
except ConfigTypeError:
|
||||
return view.get()
|
||||
else:
|
||||
return dict((key, _config_get(view[key])) for key in view.keys())
|
||||
def config_func(lib, opts, args):
|
||||
# Make sure lazy configuration is loaded
|
||||
config.resolve()
|
||||
|
||||
if not opts.defaults:
|
||||
# Remove default source
|
||||
config.sources = [source for source in config.sources if not source.default]
|
||||
|
||||
# Print paths.
|
||||
if opts.paths:
|
||||
for source in config.sources:
|
||||
if not opts.defaults and source.default:
|
||||
continue
|
||||
if source.filename:
|
||||
print(source.filename)
|
||||
|
||||
# Open in editor.
|
||||
elif opts.edit:
|
||||
path = config.user_config_path()
|
||||
|
||||
|
|
@ -1297,9 +1293,10 @@ def config_func(lib, opts, args):
|
|||
except OSError:
|
||||
raise ui.UserError("Could not edit configuration. Please"
|
||||
"set the EDITOR environment variable.")
|
||||
|
||||
# Dump configuration.
|
||||
else:
|
||||
config_dict = _config_get(config)
|
||||
print(yaml.safe_dump(config_dict, default_flow_style=False))
|
||||
print(config.dump(full=opts.defaults))
|
||||
|
||||
config_cmd.func = config_func
|
||||
default_commands.append(config_cmd)
|
||||
|
|
|
|||
|
|
@ -402,6 +402,20 @@ class ConfigView(object):
|
|||
'a list'.format(self.name)
|
||||
)
|
||||
|
||||
def flatten(self):
|
||||
"""Create a hierarchy of OrderedDicts containing the data from
|
||||
this view, recursively reifying all views to get their
|
||||
represented values.
|
||||
"""
|
||||
od = OrderedDict()
|
||||
for key, view in self.items():
|
||||
try:
|
||||
od[key] = view.flatten()
|
||||
except ConfigTypeError:
|
||||
od[key] = view.get()
|
||||
return od
|
||||
|
||||
|
||||
class RootView(ConfigView):
|
||||
"""The base of a view hierarchy. This view keeps track of the
|
||||
sources that may be accessed by subviews.
|
||||
|
|
@ -539,7 +553,7 @@ def config_dirs():
|
|||
return out
|
||||
|
||||
|
||||
# YAML.
|
||||
# YAML loading.
|
||||
|
||||
class Loader(yaml.SafeLoader):
|
||||
"""A customized YAML loader. This loader deviates from the official
|
||||
|
|
@ -606,6 +620,101 @@ def load_yaml(filename):
|
|||
raise ConfigReadError(filename, exc)
|
||||
|
||||
|
||||
# YAML dumping.
|
||||
|
||||
class Dumper(yaml.SafeDumper):
|
||||
"""A PyYAML Dumper that represents OrderedDicts as ordinary mappings
|
||||
(in order, of course).
|
||||
"""
|
||||
# From http://pyyaml.org/attachment/ticket/161/use_ordered_dict.py
|
||||
def represent_mapping(self, tag, mapping, flow_style=None):
|
||||
value = []
|
||||
node = yaml.MappingNode(tag, value, flow_style=flow_style)
|
||||
if self.alias_key is not None:
|
||||
self.represented_objects[self.alias_key] = node
|
||||
best_style = False
|
||||
if hasattr(mapping, 'items'):
|
||||
mapping = list(mapping.items())
|
||||
for item_key, item_value in mapping:
|
||||
node_key = self.represent_data(item_key)
|
||||
node_value = self.represent_data(item_value)
|
||||
if not (isinstance(node_key, yaml.ScalarNode)
|
||||
and not node_key.style):
|
||||
best_style = False
|
||||
if not (isinstance(node_value, yaml.ScalarNode)
|
||||
and not node_value.style):
|
||||
best_style = False
|
||||
value.append((node_key, node_value))
|
||||
if flow_style is None:
|
||||
if self.default_flow_style is not None:
|
||||
node.flow_style = self.default_flow_style
|
||||
else:
|
||||
node.flow_style = best_style
|
||||
return node
|
||||
|
||||
def represent_list(self, data):
|
||||
"""If a list has less than 4 items, represent it in inline style
|
||||
(i.e. comma separated, within square brackets).
|
||||
"""
|
||||
node = super(Dumper, self).represent_list(data)
|
||||
length = len(data)
|
||||
if self.default_flow_style is None and length < 4:
|
||||
node.flow_style = True
|
||||
elif self.default_flow_style is None:
|
||||
node.flow_style = False
|
||||
return node
|
||||
|
||||
def represent_bool(self, data):
|
||||
"""Represent bool as 'yes' or 'no' instead of 'true' or 'false'.
|
||||
"""
|
||||
if data:
|
||||
value = 'yes'
|
||||
else:
|
||||
value = 'no'
|
||||
return self.represent_scalar('tag:yaml.org,2002:bool', value)
|
||||
|
||||
def represent_none(self, data):
|
||||
"""Represent a None value with nothing instead of 'none'.
|
||||
"""
|
||||
return self.represent_scalar('tag:yaml.org,2002:null', '')
|
||||
|
||||
Dumper.add_representer(OrderedDict, Dumper.represent_dict)
|
||||
Dumper.add_representer(bool, Dumper.represent_bool)
|
||||
Dumper.add_representer(type(None), Dumper.represent_none)
|
||||
Dumper.add_representer(list, Dumper.represent_list)
|
||||
|
||||
def restore_yaml_comments(data, default_data):
|
||||
"""Scan default_data for comments (we include empty lines in our
|
||||
definition of comments) and place them before the same keys in data.
|
||||
Only works with comments that are on one or more own lines, i.e.
|
||||
not next to a yaml mapping.
|
||||
"""
|
||||
comment_map = dict()
|
||||
default_lines = iter(default_data.splitlines())
|
||||
for line in default_lines:
|
||||
if not line:
|
||||
comment = "\n"
|
||||
elif line.startswith("#"):
|
||||
comment = "{0}\n".format(line)
|
||||
else:
|
||||
continue
|
||||
while True:
|
||||
line = next(default_lines)
|
||||
if line and not line.startswith("#"):
|
||||
break
|
||||
comment += "{0}\n".format(line)
|
||||
key = line.split(':')[0].strip()
|
||||
comment_map[key] = comment
|
||||
out_lines = iter(data.splitlines())
|
||||
out_data = ""
|
||||
for line in out_lines:
|
||||
key = line.split(':')[0].strip()
|
||||
if key in comment_map:
|
||||
out_data += comment_map[key]
|
||||
out_data += "{0}\n".format(line)
|
||||
return out_data
|
||||
|
||||
|
||||
# Main interface.
|
||||
|
||||
class Configuration(RootView):
|
||||
|
|
@ -704,6 +813,53 @@ class Configuration(RootView):
|
|||
filename = os.path.abspath(filename)
|
||||
self.set(ConfigSource(load_yaml(filename), filename))
|
||||
|
||||
def dump(self, filename=None, full=True):
|
||||
"""Dump the Configuration object to a YAML file.
|
||||
|
||||
The order of the keys is determined from the default
|
||||
configuration file. All keys not in the default configuration
|
||||
will be appended to the end of the file.
|
||||
|
||||
:param filename: The file to dump the configuration to, or None
|
||||
if the YAML string should be returned instead
|
||||
:type filename: unicode
|
||||
:param full: Dump settings that don't differ from the defaults
|
||||
as well
|
||||
"""
|
||||
out_dict = OrderedDict()
|
||||
default_conf = next(x for x in self.sources if x.default)
|
||||
try:
|
||||
default_keys = list(default_conf.keys())
|
||||
except AttributeError:
|
||||
default_keys = []
|
||||
new_keys = [x for x in self.keys() if not x in default_keys]
|
||||
out_keys = default_keys + new_keys
|
||||
for key in out_keys:
|
||||
# Skip entries unchanged from default config
|
||||
if (not full and key in default_keys
|
||||
and self[key].get() == default_conf[key]):
|
||||
continue
|
||||
try:
|
||||
out_dict[key] = self[key].flatten()
|
||||
except ConfigTypeError:
|
||||
out_dict[key] = self[key].get()
|
||||
|
||||
yaml_out = yaml.dump(out_dict, Dumper=Dumper,
|
||||
default_flow_style=None, indent=4,
|
||||
width=1000)
|
||||
|
||||
# Restore comments to the YAML text.
|
||||
with open(default_conf.filename, 'r') as fp:
|
||||
default_data = fp.read()
|
||||
yaml_out = restore_yaml_comments(yaml_out, default_data)
|
||||
|
||||
# Return the YAML or write it to a file.
|
||||
if filename is None:
|
||||
return yaml_out
|
||||
else:
|
||||
with open(filename, 'w') as fp:
|
||||
fp.write(yaml_out)
|
||||
|
||||
|
||||
class LazyConfig(Configuration):
|
||||
"""A Configuration at reads files on demand when it is first
|
||||
|
|
|
|||
Loading…
Reference in a new issue