From 576ec92992036512853deb1b4e03fb92d9dff5f7 Mon Sep 17 00:00:00 2001 From: Jack Wilsdon Date: Sun, 1 May 2016 21:21:40 +0100 Subject: [PATCH 1/7] 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) From b4715d61e19d1ab50b918d53610b10026ed95a92 Mon Sep 17 00:00:00 2001 From: Jack Wilsdon Date: Sun, 1 May 2016 21:30:20 +0100 Subject: [PATCH 2/7] Fix over-indentation of wrapped code --- beetsplug/hook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/hook.py b/beetsplug/hook.py index 3a3c8ea84..d3a1fbdb8 100644 --- a/beetsplug/hook.py +++ b/beetsplug/hook.py @@ -96,7 +96,7 @@ class CodingFormatter(AutoFieldCountFormatter): def convert_field(self, value, conversion): converted = super(CodingFormatter, self).convert_field(value, - conversion) + conversion) try: converted = converted.decode(self._coding) except UnicodeEncodeError: From e66981c4d837f8a8f93ce9fde17c17b57cb954d0 Mon Sep 17 00:00:00 2001 From: Jack Wilsdon Date: Sun, 1 May 2016 21:31:53 +0100 Subject: [PATCH 3/7] Use beets shlex_split instead of shlex.split --- beetsplug/hook.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/beetsplug/hook.py b/beetsplug/hook.py index d3a1fbdb8..b2eec360c 100644 --- a/beetsplug/hook.py +++ b/beetsplug/hook.py @@ -16,11 +16,11 @@ from __future__ import division, absolute_import, print_function import string -import shlex import subprocess from beets.plugins import BeetsPlugin from beets.ui import _arg_encoding +from beets.util import shlex_split # Sadly we need this class for {} support due to issue 13598 @@ -134,17 +134,13 @@ class HookPlugin(BeetsPlugin): 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) + command_pieces = shlex_split(formatted_command) self._log.debug(u'running command "{0}" for event {1}', formatted_command, event) try: - subprocess.Popen(decoded_command_pieces).wait() + subprocess.Popen(command_pieces).wait() except OSError as exc: self._log.error(u'hook for {0} failed: {1}', event, exc) From ba7004de6e731275a57749ef7aa3a2a8bfe75502 Mon Sep 17 00:00:00 2001 From: Jack Wilsdon Date: Sun, 1 May 2016 21:33:06 +0100 Subject: [PATCH 4/7] Remove unneeded AutoFieldCountFormatter --- beetsplug/hook.py | 60 +---------------------------------------------- 1 file changed, 1 insertion(+), 59 deletions(-) diff --git a/beetsplug/hook.py b/beetsplug/hook.py index b2eec360c..a0faf4fe2 100644 --- a/beetsplug/hook.py +++ b/beetsplug/hook.py @@ -23,65 +23,7 @@ from beets.ui import _arg_encoding from beets.util import shlex_split -# 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): +class CodingFormatter(string.Formatter): def __init__(self, coding): self._coding = coding From 63caf1fceb94d185e73858c2b58c82bf5912b7c4 Mon Sep 17 00:00:00 2001 From: Jack Wilsdon Date: Sun, 1 May 2016 21:44:05 +0100 Subject: [PATCH 5/7] Add documentation for coding formatter --- beetsplug/hook.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/beetsplug/hook.py b/beetsplug/hook.py index a0faf4fe2..99d7dc37d 100644 --- a/beetsplug/hook.py +++ b/beetsplug/hook.py @@ -24,10 +24,22 @@ from beets.util import shlex_split class CodingFormatter(string.Formatter): + """A custom string formatter that decodes the format string and it's + fields. + """ + def __init__(self, coding): + """Creates a new coding formatter with the provided coding.""" self._coding = coding def format(self, format_string, *args, **kwargs): + """Formats the provided string using the provided arguments and keyword + arguments. + + This method decodes the format string using the formatter's coding. + + See str.format and string.Formatter.format. + """ try: format_string = format_string.decode(self._coding) except UnicodeEncodeError: @@ -37,6 +49,12 @@ class CodingFormatter(string.Formatter): **kwargs) def convert_field(self, value, conversion): + """Converts the provided value given a conversion type. + + This method decodes the converted value using the formatter's coding. + + See string.Formatter.convert_field. + """ converted = super(CodingFormatter, self).convert_field(value, conversion) try: From 9f0db136752bd3feec37a5c28961e265ef3d5679 Mon Sep 17 00:00:00 2001 From: Jack Wilsdon Date: Sun, 1 May 2016 21:44:40 +0100 Subject: [PATCH 6/7] Remove unnecessary variable --- beetsplug/hook.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/beetsplug/hook.py b/beetsplug/hook.py index 99d7dc37d..2085e64c5 100644 --- a/beetsplug/hook.py +++ b/beetsplug/hook.py @@ -90,8 +90,7 @@ class HookPlugin(BeetsPlugin): self._log.error('invalid command "{0}"', command) return - encoding = _arg_encoding() - formatter = CodingFormatter(encoding) + formatter = CodingFormatter(_arg_encoding()) formatted_command = formatter.format(command, event=event, **kwargs) command_pieces = shlex_split(formatted_command) From 92af723682501b1f3550f52502c9079990370298 Mon Sep 17 00:00:00 2001 From: Jack Wilsdon Date: Sun, 1 May 2016 23:04:22 +0100 Subject: [PATCH 7/7] Format individual command pieces instead of whole command --- beetsplug/hook.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/beetsplug/hook.py b/beetsplug/hook.py index 2085e64c5..14ccbf099 100644 --- a/beetsplug/hook.py +++ b/beetsplug/hook.py @@ -91,12 +91,14 @@ class HookPlugin(BeetsPlugin): return formatter = CodingFormatter(_arg_encoding()) - formatted_command = formatter.format(command, event=event, - **kwargs) - command_pieces = shlex_split(formatted_command) + command_pieces = shlex_split(command) + + for i, piece in enumerate(command_pieces): + command_pieces[i] = formatter.format(piece, event=event, + **kwargs) self._log.debug(u'running command "{0}" for event {1}', - formatted_command, event) + u' '.join(command_pieces), event) try: subprocess.Popen(command_pieces).wait()