move show_model_changes to ui package

This makes it more naturally reusable for plugins.
This commit is contained in:
Adrian Sampson 2014-02-22 15:06:16 -08:00
parent b383ce3450
commit 1253cb695d
4 changed files with 97 additions and 70 deletions

View file

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

View file

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

View file

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

View file

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