mirror of
https://github.com/beetbox/beets.git
synced 2026-02-21 14:56:02 +01:00
move show_model_changes to ui package
This makes it more naturally reusable for plugins.
This commit is contained in:
parent
b383ce3450
commit
1253cb695d
4 changed files with 97 additions and 70 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue