# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. """Allows inline path template customization code in the config file. """ from __future__ import division, absolute_import, print_function import traceback import itertools from beets.plugins import BeetsPlugin from beets import config import six FUNC_NAME = u'__INLINE_FUNC__' class InlineError(Exception): """Raised when a runtime error occurs in an inline expression. """ def __init__(self, code, exc): super(InlineError, self).__init__( (u"error in inline path field code:\n" u"%s\n%s: %s") % (code, type(exc).__name__, six.text_type(exc)) ) def _compile_func(body): """Given Python code for a function body, return a compiled callable that invokes that code. """ 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] class InlinePlugin(BeetsPlugin): def __init__(self): super(InlinePlugin, self).__init__() config.add({ 'pathfields': {}, # Legacy name. 'item_fields': {}, 'album_fields': {}, }) # Item fields. for key, view in itertools.chain(config['item_fields'].items(), config['pathfields'].items()): self._log.debug(u'adding item field {0}', key) func = self.compile_inline(view.as_str(), False) if func is not None: self.template_fields[key] = func # Album fields. for key, view in config['album_fields'].items(): self._log.debug(u'adding album field {0}', key) func = self.compile_inline(view.as_str(), True) if func is not None: self.album_template_fields[key] = func def compile_inline(self, python_code, album): """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: func = _compile_func(python_code) except SyntaxError: self._log.error(u'syntax error in inline field definition:\n' u'{0}', traceback.format_exc()) return else: is_expr = False else: is_expr = True def _dict_for(obj): out = dict(obj) if album: out['items'] = list(obj.items()) return out if is_expr: # For expressions, just evaluate and return the result. def _expr_func(obj): values = _dict_for(obj) 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(obj): func.__globals__.update(_dict_for(obj)) try: return func() except Exception as exc: raise InlineError(python_code, exc) return _func_func