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:
Adrian Sampson 2014-02-28 09:47:03 -08:00
parent 692645466e
commit 3b2d51e018
2 changed files with 167 additions and 14 deletions

View file

@ -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)

View file

@ -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