mirror of
https://github.com/beetbox/beets.git
synced 2025-12-28 11:32:30 +01:00
GH-72: use function bodies instead of special var
This commit is contained in:
parent
14b5170aec
commit
f7bef39769
3 changed files with 87 additions and 46 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
----------------------
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue