From 1253cb695d14a4281eef7f22570168b91037771d Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 22 Feb 2014 15:06:16 -0800 Subject: [PATCH] move show_model_changes to ui package This makes it more naturally reusable for plugins. --- beets/ui/__init__.py | 90 +++++++++++++++++++++++++++++++++++++++++++- beets/ui/commands.py | 72 +++-------------------------------- beetsplug/mbsync.py | 2 +- test/test_ui.py | 3 +- 4 files changed, 97 insertions(+), 70 deletions(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 0ed8302e3..b56446032 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Adrian Sampson. +# Copyright 2014, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -50,8 +50,10 @@ if sys.platform == 'win32': colorama.init() + # Constants. + PF_KEY_QUERIES = { 'comp': 'comp:true', 'singleton': 'singleton:true', @@ -66,8 +68,10 @@ class UserError(Exception): log = logging.getLogger('beets') + # Utilities. + def _encoding(): """Tries to guess the encoding used by the terminal.""" # Configured override? @@ -83,12 +87,14 @@ def _encoding(): # failing entirely for no good reason, assume UTF-8. return 'utf8' + def decargs(arglist): """Given a list of command-line argument bytestrings, attempts to decode them to Unicode strings. """ return [s.decode(_encoding()) for s in arglist] + def print_(*strings): """Like print, but rather than raising an error when a character is not in the terminal's encoding's character set, just silently @@ -105,6 +111,7 @@ def print_(*strings): txt = txt.encode(_encoding(), 'replace') print(txt) + def input_(prompt=None): """Like `raw_input`, but decodes the result to a Unicode string. Raises a UserError if stdin is not available. The prompt is sent to @@ -126,6 +133,7 @@ def input_(prompt=None): return resp.decode(sys.stdin.encoding or 'utf8', 'ignore') + def input_options(options, require=False, prompt=None, fallback_prompt=None, numrange=None, default=None, max_width=72): """Prompts a user for input. The sequence of `options` defines the @@ -284,6 +292,7 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None, # Prompt for new input. resp = input_(fallback_prompt) + def input_yn(prompt, require=False): """Prompts the user for a "yes" or "no" response. The default is "yes" unless `require` is `True`, in which case there is no default. @@ -293,6 +302,7 @@ def input_yn(prompt, require=False): ) return sel == 'y' + def human_bytes(size): """Formats size, a number of bytes, in a human-readable way.""" suffices = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'HB'] @@ -302,6 +312,7 @@ def human_bytes(size): size /= 1024.0 return "big" + def human_seconds(interval): """Formats interval, a number of seconds, as a human-readable time interval using English words. @@ -328,6 +339,7 @@ def human_seconds(interval): return "%3.1f %ss" % (interval, suffix) + def human_seconds_short(interval): """Formats a number of seconds as a short human-readable M:SS string. @@ -335,6 +347,7 @@ def human_seconds_short(interval): interval = int(interval) return u'%i:%02i' % (interval // 60, interval % 60) + # ANSI terminal colorization code heavily inspired by pygments: # http://dev.pocoo.org/hg/pygments-main/file/b2deea5b5030/pygments/console.py # (pygments is by Tim Hatch, Armin Ronacher, et al.) @@ -357,6 +370,7 @@ def _colorize(color, text): raise ValueError('no such color %s', color) return escape + text + RESET_COLOR + def colorize(color, text): """Colorize text if colored output is enabled. (Like _colorize but conditional.) @@ -366,6 +380,7 @@ def colorize(color, text): else: return text + def _colordiff(a, b, highlight='red', minor_highlight='lightgray'): """Given two values, return the same pair of strings except with their differences highlighted in the specified color. Strings are @@ -415,6 +430,7 @@ def _colordiff(a, b, highlight='red', minor_highlight='lightgray'): return u''.join(a_out), u''.join(b_out) + def colordiff(a, b, highlight='red'): """Colorize differences between two values if color is enabled. (Like _colordiff but conditional.) @@ -424,6 +440,7 @@ def colordiff(a, b, highlight='red'): else: return unicode(a), unicode(b) + def color_diff_suffix(a, b, highlight='red'): """Colorize the differing suffix between two strings.""" a, b = unicode(a), unicode(b) @@ -447,6 +464,7 @@ def color_diff_suffix(a, b, highlight='red'): return a[:first_diff] + colorize(highlight, a[first_diff:]), \ b[:first_diff] + colorize(highlight, b[first_diff:]) + def get_path_formats(subview=None): """Get the configuration's path formats as a list of query/template pairs. @@ -458,6 +476,7 @@ def get_path_formats(subview=None): path_formats.append((query, Template(view.get(unicode)))) return path_formats + def get_replacements(): """Confit validation function that reads regex/string pairs. """ @@ -474,6 +493,7 @@ def get_replacements(): ) return replacements + def get_plugin_paths(): """Get the list of search paths for plugins from the config file. The value for "pluginpath" may be a single string or a list of @@ -488,6 +508,7 @@ def get_plugin_paths(): ) return map(util.normpath, pluginpaths) + def _pick_format(album, fmt=None): """Pick a format string for printing Album or Item objects, falling back to config options and defaults. @@ -499,6 +520,7 @@ def _pick_format(album, fmt=None): else: return config['list_format_item'].get(unicode) + def print_obj(obj, lib, fmt=None): """Print an Album or Item object. If `fmt` is specified, use that format string. Otherwise, use the configured template. @@ -511,6 +533,7 @@ def print_obj(obj, lib, fmt=None): template = Template(fmt) print_(obj.evaluate_template(template)) + def term_width(): """Get the width (columns) of the terminal.""" fallback = config['ui']['terminal_width'].get(int) @@ -534,8 +557,73 @@ def term_width(): return width +FLOAT_EPSILON = 0.01 +def _field_diff(field, old, new): + """Given two Model objects, format their values for `field` and + highlight changes among them. Return a human-readable string. If the + value has not changed, return None instead. + """ + oldval = old.get(field) + newval = new.get(field) + + # If no change, abort. + if isinstance(oldval, float) and isinstance(newval, float) and \ + abs(oldval - newval) < FLOAT_EPSILON: + return None + elif oldval == newval: + return None + + # Get formatted values for output. + oldstr = old._get_formatted(field) + newstr = new._get_formatted(field) + + # For strings, highlight changes. For others, colorize the whole + # thing. + if isinstance(oldval, basestring): + oldstr, newstr = colordiff(oldval, newval) + else: + oldstr, newstr = colorize('red', oldstr), colorize('red', newstr) + + return u'{0} -> {1}'.format(oldstr, newstr) + + +def show_model_changes(new, old=None, fields=None, always=False): + """Given a Model object, print a list of changes from its pristine + version stored in the database. Return a boolean indicating whether + any changes were found. + + `old` may be the "original" object to avoid using the pristine + version from the database. `fields` may be a list of fields to + restrict the detection to. `always` indicates whether the object is + always identified, regardless of whether any changes are present. + """ + old = old or new._db._get(type(new), new.id) + + # Build up lines showing changes. + changes = [] + for field in old: + # Subset of the fields. Never show mtime. + if field == 'mtime' or (fields and field not in fields): + continue + + # Detect and show difference for this field. + line = _field_diff(field, old, new) + if line: + changes.append(u' {0}: {1}'.format(field, line)) + + # Print changes. + if changes or always: + print_obj(old, old._db) + if changes: + print_(u'\n'.join(changes)) + + return bool(changes) + + + # Subcommand parsing infrastructure. + # This is a fairly generic subcommand parser for optparse. It is # maintained externally here: # http://gist.github.com/462717 diff --git a/beets/ui/commands.py b/beets/ui/commands.py index d6caf9ff0..84ae353d3 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -75,69 +75,6 @@ def _do_query(lib, query, album, also_items=True): return items, albums -FLOAT_EPSILON = 0.01 -def _field_diff(field, old, new): - """Given two Model objects, format their values for `field` and - highlight changes among them. Return a human-readable string. If the - value has not changed, return None instead. - """ - oldval = old.get(field) - newval = new.get(field) - - # If no change, abort. - if isinstance(oldval, float) and isinstance(newval, float) and \ - abs(oldval - newval) < FLOAT_EPSILON: - return None - elif oldval == newval: - return None - - # Get formatted values for output. - oldstr = old._get_formatted(field) - newstr = new._get_formatted(field) - - # For strings, highlight changes. For others, colorize the whole - # thing. - if isinstance(oldval, basestring): - oldstr, newstr = ui.colordiff(oldval, newval) - else: - oldstr, newstr = ui.colorize('red', oldstr), ui.colorize('red', newstr) - - return u'{0} -> {1}'.format(oldstr, newstr) - - -def _show_model_changes(new, old=None, fields=None, always=False): - """Given a Model object, print a list of changes from its pristine - version stored in the database. Return a boolean indicating whether - any changes were found. - - `old` may be the "original" object to avoid using the pristine - version from the database. `fields` may be a list of fields to - restrict the detection to. `always` indicates whether the object is - always identified, regardless of whether any changes are present. - """ - old = old or new._db._get(type(new), new.id) - - # Build up lines showing changes. - changes = [] - for field in old: - # Subset of the fields. Never show mtime. - if field == 'mtime' or (fields and field not in fields): - continue - - # Detect and show difference for this field. - line = _field_diff(field, old, new) - if line: - changes.append(u' {0}: {1}'.format(field, line)) - - # Print changes. - if changes or always: - ui.print_obj(old, old._db) - if changes: - ui.print_(u'\n'.join(changes)) - - return bool(changes) - - # fields: Shows a list of available fields for queries and format strings. @@ -984,7 +921,8 @@ def update_items(lib, query, album, move, pretend): item._dirty.discard('albumartist') # Check for and display changes. - changed = _show_model_changes(item, fields=library.ITEM_KEYS_META) + changed = ui.show_model_changes(item, + fields=library.ITEM_KEYS_META) # Save changes. if not pretend: @@ -1163,7 +1101,7 @@ def modify_items(lib, mods, query, write, move, album, confirm): for obj in objs: for field, value in fsets.iteritems(): obj[field] = value - if _show_model_changes(obj): + if ui.show_model_changes(obj): changed.add(obj) # Still something to do? @@ -1289,8 +1227,8 @@ def write_items(lib, query, pretend): continue # Check for and display changes. - changed = _show_model_changes(item, clean_item, library.ITEM_KEYS_META, - always=True) + changed = ui.show_model_changes(item, clean_item, + library.ITEM_KEYS_META, always=True) if changed and not pretend: try: item.write() diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index d2182e825..8df913472 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -28,7 +28,7 @@ def _print_and_apply_changes(lib, item, old_data, move, pretend, write): """Apply changes to an Item and preview them in the console. Return a boolean indicating whether any changes were made. """ - changed = ui.commands._show_model_changes(item) + changed = ui.show_model_changes(item) if not changed: return False diff --git a/test/test_ui.py b/test/test_ui.py index 4c68c3dc6..771eab84b 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -30,6 +30,7 @@ from beets.mediafile import MediaFile from beets import config from beets import plugins + class ListTest(_common.TestCase): def setUp(self): super(ListTest, self).setUp() @@ -723,7 +724,7 @@ class ShowModelChangeTest(_common.TestCase): self.a.path = self.b.path def _show(self, **kwargs): - change = commands._show_model_changes(self.a, self.b, **kwargs) + change = ui.show_model_changes(self.a, self.b, **kwargs) out = self.io.getoutput() return change, out