From 576ec92992036512853deb1b4e03fb92d9dff5f7 Mon Sep 17 00:00:00 2001 From: Jack Wilsdon Date: Sun, 1 May 2016 21:21:40 +0100 Subject: [PATCH] Fix encoding for hook plugin - Add a AutoFieldCountFormatter formatter for auto field incrementation. - Add a CodingFormatter for formatting encoded strings. - Fix encoding for hook plugin using CodingFormatter. --- beetsplug/hook.py | 105 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 96 insertions(+), 9 deletions(-) diff --git a/beetsplug/hook.py b/beetsplug/hook.py index 2b0bf2b0a..3a3c8ea84 100644 --- a/beetsplug/hook.py +++ b/beetsplug/hook.py @@ -15,6 +15,7 @@ """Allows custom commands to be run when an event is emitted by beets""" from __future__ import division, absolute_import, print_function +import string import shlex import subprocess @@ -22,6 +23,88 @@ from beets.plugins import BeetsPlugin from beets.ui import _arg_encoding +# Sadly we need this class for {} support due to issue 13598 +# https://bugs.python.org/issue13598 +# https://bugs.python.org/file25816/issue13598.diff +class AutoFieldCountFormatter(string.Formatter): + def _vformat(self, format_string, args, kwargs, used_args, + recursion_depth): + if recursion_depth < 0: + raise ValueError('Max string recursion exceeded') + auto_field_count = 0 + # manual numbering + manual = None + result = [] + for literal_text, field_name, format_spec, conversion in \ + self.parse(format_string): + + # output the literal text + if literal_text: + result.append(literal_text) + + # if there's a field, output it + if field_name is not None: + # this is some markup, find the object and do + # the formatting + + # ensure we are consistent with numbering + if (field_name == "" and manual) or manual is False: + raise ValueError("cannot switch from manual field " + + "specification to automatic field " + + "numbering") + + # automatic numbering + if field_name == "": + manual = False + field_name = str(auto_field_count) + auto_field_count += 1 + + # manual numbering + else: + manual = True + + # given the field_name, find the object it references + # and the argument it came from + obj, arg_used = self.get_field(field_name, args, kwargs) + used_args.add(arg_used) + + # do any conversion on the resulting object + obj = self.convert_field(obj, conversion) + + # expand the format spec, if needed + format_spec = self._vformat(format_spec, args, kwargs, + used_args, recursion_depth - 1) + + # format the object and append to the result + result.append(self.format_field(obj, format_spec)) + + return ''.join(result) + + +class CodingFormatter(AutoFieldCountFormatter): + def __init__(self, coding): + self._coding = coding + + def format(self, format_string, *args, **kwargs): + try: + format_string = format_string.decode(self._coding) + except UnicodeEncodeError: + pass + + return super(CodingFormatter, self).format(format_string, *args, + **kwargs) + + def convert_field(self, value, conversion): + converted = super(CodingFormatter, self).convert_field(value, + conversion) + try: + converted = converted.decode(self._coding) + except UnicodeEncodeError: + pass + + return converted + + class HookPlugin(BeetsPlugin): """Allows custom commands to be run when an event is emitted by beets""" def __init__(self): @@ -47,18 +130,22 @@ class HookPlugin(BeetsPlugin): self._log.error('invalid command "{0}"', command) return - unicode_command = command.decode('utf-8') - formatted_command = unicode_command.format(event=event, - **kwargs) - encoded_command = formatted_command.decode(_arg_encoding()) - command_pieces = shlex.split(encoded_command) + encoding = _arg_encoding() + formatter = CodingFormatter(encoding) + formatted_command = formatter.format(command, event=event, + **kwargs) + encoded_formatted_command = formatted_command.encode(encoding) + command_pieces = shlex.split(encoded_formatted_command) + decoded_command_pieces = map(lambda piece: + piece.decode(encoding), + command_pieces) - self._log.debug('Running command "{0}" for event {1}', - encoded_command, event) + self._log.debug(u'running command "{0}" for event {1}', + formatted_command, event) try: - subprocess.Popen(command_pieces).wait() + subprocess.Popen(decoded_command_pieces).wait() except OSError as exc: - self._log.error('hook for {0} failed: {1}', event, exc) + self._log.error(u'hook for {0} failed: {1}', event, exc) self.register_listener(event, hook_function)