GH-72: use function bodies instead of special var

This commit is contained in:
Adrian Sampson 2013-01-05 17:48:00 -08:00
parent 14b5170aec
commit f7bef39769
3 changed files with 87 additions and 46 deletions

View file

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

View file

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

View file

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