diff --git a/beetsplug/inline.py b/beetsplug/inline.py index ac9a2665c..2308d1512 100644 --- a/beetsplug/inline.py +++ b/beetsplug/inline.py @@ -22,46 +22,71 @@ from beets import config log = logging.getLogger('beets') +FUNC_NAME = u'__INLINE_FUNC__' + class InlineError(Exception): """Raised when a runtime error occurs in an inline expression. """ - def __init__(self, expr, exc): + def __init__(self, code, exc): super(InlineError, self).__init__( - (u"error in inline path field expression:\n" \ - u"%s\n%s: %s") % (expr, type(exc).__name__, unicode(exc)) + (u"error in inline path field code:\n" \ + u"%s\n%s: %s") % (code, type(exc).__name__, unicode(exc)) ) -def compile_expr(expr): - """Given a Python expression, compile it as a path field function. - The returned function takes a single argument, an Item, and returns - a Unicode string. If the expression cannot be compiled, then an - error is logged and this function returns None. +def _compile_func(body): + """Given Python code for a function body, return a compiled + callable that invokes that code. """ - code = None - try: - code = compile(u'(%s)' % expr, 'inline', 'eval') - except SyntaxError: - try: - code = compile(expr, 'inline', 'exec') - except SyntaxError: - log.error(u'syntax error in field expression:\n%s' % - traceback.format_exc()) - if code == None: - return None + body = u'def {0}():\n {1}'.format( + FUNC_NAME, + body.replace('\n', '\n ') + ) + code = compile(body, 'inline', 'exec') + env = {} + eval(code, env) + return env[FUNC_NAME] - def field_func(item): - values = dict(item.record) +def compile_inline(python_code): + """Given a Python expression or function body, compile it as a path + field function. The returned function takes a single argument, an + Item, and returns a Unicode string. If the expression cannot be + compiled, then an error is logged and this function returns None. + """ + # First, try compiling as a single function. + try: + code = compile(u'({0})'.format(python_code), 'inline', 'eval') + except SyntaxError: + # Fall back to a function body. try: - ret = eval(code, values) - if ret == None: - ret = values.get('_', None) - if ret == None: - raise Exception('Expression must be a statement or a block of' \ - ' code storing the result in the "_" variable.') - return ret - except Exception as exc: - raise InlineError(expr, exc) - return field_func + func = _compile_func(python_code) + except SyntaxError: + log.error(u'syntax error in inline field definition:\n%s' % + traceback.format_exc()) + return + else: + is_expr = False + else: + is_expr = True + + if is_expr: + # For expressions, just evaluate and return the result. + def _expr_func(item): + values = dict(item.record) + try: + return eval(code, values) + except Exception as exc: + raise InlineError(python_code, exc) + return _expr_func + else: + # For function bodies, invoke the function with values as global + # variables. + def _func_func(item): + func.__globals__.update(item.record) + try: + return func() + except Exception as exc: + raise InlineError(python_code, exc) + return _func_func class InlinePlugin(BeetsPlugin): template_fields = {} @@ -76,6 +101,6 @@ class InlinePlugin(BeetsPlugin): # Add field expressions. for key, view in config['pathfields'].items(): log.debug(u'adding template field %s' % key) - func = compile_expr(view.get(unicode)) + func = compile_inline(view.get(unicode)) if func is not None: InlinePlugin.template_fields[key] = func diff --git a/docs/changelog.rst b/docs/changelog.rst index 51a52bb2f..8edb5c09d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -20,8 +20,8 @@ This release entirely revamps beets' configuration system. It also adds some new features: -* :doc:`/plugins/inline`: Inline definitions can now be statements or blocks - in addition to just expressions. Thanks to Florent Thoumie. +* :doc:`/plugins/inline`: Inline definitions can now contain statements or + blocks in addition to just expressions. Thanks to Florent Thoumie. 1.0.0 (in development) ---------------------- diff --git a/docs/plugins/inline.rst b/docs/plugins/inline.rst index 02d5510d3..2537dbebb 100644 --- a/docs/plugins/inline.rst +++ b/docs/plugins/inline.rst @@ -1,28 +1,44 @@ Inline Plugin ============= -The ``inline`` plugin lets you use Python expressions to customize your path -formats. Using it, you can define template fields in your beets configuration -file and refer to them from your template strings in the ``[paths]`` section -(see :doc:`/reference/config/`). +The ``inline`` plugin lets you use Python to customize your path formats. Using +it, you can define template fields in your beets configuration file and refer +to them from your template strings in the ``[paths]`` section (see +:doc:`/reference/config/`). To use inline field definitions, first enable the plugin by putting ``inline`` on your ``plugins`` line in your configuration file. Then, make a -``pathfields:`` block in your config file. Under this key, every line -defines a new template field; the key is the name of the field (you'll use the -name to refer to the field in your templates) and the value is a Python -expression. The expression has all of a track's fields in scope, so you can +``pathfields:`` block in your config file. Under this key, every line defines a +new template field; the key is the name of the field (you'll use the name to +refer to the field in your templates) and the value is a Python expression or +function body. The Python code has all of a track's fields in scope, so you can refer to any normal attributes (such as ``artist`` or ``title``) as Python -variables. Here are a couple of examples:: +variables. + +Here are a couple of examples of expressions:: pathfields: initial: albumartist[0].upper() + u'.' disc_and_track: u'%02i.%02i' % (disc, track) if disctotal > 1 else u'%02i' % (track) -Note that YAML syntax allows newlines in values if the subsequent -lines are indented. These examples define ``$initial`` and -``$disc_and_track`` fields that can be referenced in path templates like so:: +Note that YAML syntax allows newlines in values if the subsequent lines are +indented. + +These examples define ``$initial`` and ``$disc_and_track`` fields that can be +referenced in path templates like so:: paths: default: $initial/$artist/$album%aunique{}/$disc_and_track $title + +If you need to use statements like ``import``, you can write a Python function +body instead of a single expression. In this case, you'll need to ``return`` +a result for the value of the path field. Here's a silly, contrived example:: + + pathfields: + track_radius: | + import math + return 2.0 * math.pi * track + +You might want to use the YAML syntax for "block literals," in which a leading +``|`` character indicates a multi-line block of text.