Merge pull request #3150 from beetbox/named-query

Add support for "named queries" and use them in the playlist plugin
This commit is contained in:
Adrian Sampson 2019-02-17 15:08:31 -05:00 committed by GitHub
commit 9320db21eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 93 additions and 34 deletions

View file

@ -143,6 +143,11 @@ class Model(object):
are subclasses of `Sort`.
"""
_queries = {}
"""Named queries that use a field-like `name:value` syntax but which
do not relate to any specific field.
"""
_always_dirty = False
"""By default, fields only become "dirty" when their value actually
changes. Enabling this flag marks fields as dirty even when the new

View file

@ -119,12 +119,13 @@ def construct_query_part(model_cls, prefixes, query_part):
if not query_part:
return query.TrueQuery()
# Use `model_cls` to build up a map from field names to `Query`
# classes.
# Use `model_cls` to build up a map from field (or query) names to
# `Query` classes.
query_classes = {}
for k, t in itertools.chain(model_cls._fields.items(),
model_cls._types.items()):
query_classes[k] = t.query
query_classes.update(model_cls._queries) # Non-field queries.
# Parse the string.
key, pattern, query_class, negate = \
@ -137,26 +138,27 @@ def construct_query_part(model_cls, prefixes, query_part):
# The query type matches a specific field, but none was
# specified. So we use a version of the query that matches
# any field.
q = query.AnyFieldQuery(pattern, model_cls._search_fields,
query_class)
if negate:
return query.NotQuery(q)
else:
return q
out_query = query.AnyFieldQuery(pattern, model_cls._search_fields,
query_class)
else:
# Non-field query type.
if negate:
return query.NotQuery(query_class(pattern))
else:
return query_class(pattern)
out_query = query_class(pattern)
# Otherwise, this must be a `FieldQuery`. Use the field name to
# construct the query object.
key = key.lower()
q = query_class(key.lower(), pattern, key in model_cls._fields)
# Field queries get constructed according to the name of the field
# they are querying.
elif issubclass(query_class, query.FieldQuery):
key = key.lower()
out_query = query_class(key.lower(), pattern, key in model_cls._fields)
# Non-field (named) query.
else:
out_query = query_class(pattern)
# Apply negation.
if negate:
return query.NotQuery(q)
return q
return query.NotQuery(out_query)
else:
return out_query
def query_from_strings(query_cls, model_cls, prefixes, query_parts):

View file

@ -344,6 +344,16 @@ def types(model_cls):
return types
def named_queries(model_cls):
# Gather `item_queries` and `album_queries` from the plugins.
attr_name = '{0}_queries'.format(model_cls.__name__.lower())
queries = {}
for plugin in find_plugins():
plugin_queries = getattr(plugin, attr_name, {})
queries.update(plugin_queries)
return queries
def track_distance(item, info):
"""Gets the track distance calculated by all loaded plugins.
Returns a Distance object.

View file

@ -1143,8 +1143,12 @@ def _setup(options, lib=None):
if lib is None:
lib = _open_library(config)
plugins.send("library_opened", lib=lib)
# Add types and queries defined by plugins.
library.Item._types.update(plugins.types(library.Item))
library.Album._types.update(plugins.types(library.Album))
library.Item._queries.update(plugins.named_queries(library.Item))
library.Album._queries.update(plugins.named_queries(library.Album))
return subcommands, plugins, lib

View file

@ -19,11 +19,11 @@ import fnmatch
import beets
class PlaylistQuery(beets.dbcore.FieldQuery):
class PlaylistQuery(beets.dbcore.Query):
"""Matches files listed by a playlist file.
"""
def __init__(self, field, pattern, fast=True):
super(PlaylistQuery, self).__init__(field, pattern, fast)
def __init__(self, pattern):
self.pattern = pattern
config = beets.config['playlist']
# Get the full path to the playlist
@ -76,14 +76,8 @@ class PlaylistQuery(beets.dbcore.FieldQuery):
return item.path in self.paths
class PlaylistType(beets.dbcore.types.String):
"""Custom type for playlist query.
"""
query = PlaylistQuery
class PlaylistPlugin(beets.plugins.BeetsPlugin):
item_types = {'playlist': PlaylistType()}
item_queries = {'playlist': PlaylistQuery}
def __init__(self):
super(PlaylistPlugin, self).__init__()

View file

@ -144,6 +144,14 @@ Fixes:
.. _python-itunes: https://github.com/ocelma/python-itunes
For developers:
* In addition to prefix-based field queries, plugins can now define *named
queries* that are not associated with any specific field.
For example, the new :doc:`/plugins/playlist` supports queries like
``playlist:name`` although there is no field named ``playlist``.
See :ref:`extend-query` for details.
1.4.7 (May 29, 2018)
--------------------

View file

@ -443,15 +443,24 @@ Extend the Query Syntax
^^^^^^^^^^^^^^^^^^^^^^^
You can add new kinds of queries to beets' :doc:`query syntax
</reference/query>` indicated by a prefix. As an example, beets already
</reference/query>`. There are two ways to add custom queries: using a prefix
and using a name. Prefix-based query extension can apply to *any* field, while
named queries are not associated with any field. For example, beets already
supports regular expression queries, which are indicated by a colon
prefix---plugins can do the same.
To do so, define a subclass of the ``Query`` type from the
``beets.dbcore.query`` module. Then, in the ``queries`` method of your plugin
class, return a dictionary mapping prefix strings to query classes.
For either kind of query extension, define a subclass of the ``Query`` type
from the ``beets.dbcore.query`` module. Then:
One simple kind of query you can extend is the ``FieldQuery``, which
- To define a prefix-based query, define a ``queries`` method in your plugin
class. Return from this method a dictionary mapping prefix strings to query
classes.
- To define a named query, defined dictionaries named either ``item_queries``
or ``album_queries``. These should map names to query types. So if you
use ``{ "foo": FooQuery }``, then the query ``foo:bar`` will construct a
query like ``FooQuery("bar")``.
For prefix-based queries, you will want to extend ``FieldQuery``, which
implements string comparisons on fields. To use it, create a subclass
inheriting from that class and override the ``value_match`` class method.
(Remember the ``@classmethod`` decorator!) The following example plugin

View file

@ -222,12 +222,19 @@ class TestHelper(object):
beets.config['plugins'] = plugins
beets.plugins.load_plugins(plugins)
beets.plugins.find_plugins()
# Take a backup of the original _types to restore when unloading
# Take a backup of the original _types and _queries to restore
# when unloading.
Item._original_types = dict(Item._types)
Album._original_types = dict(Album._types)
Item._types.update(beets.plugins.types(Item))
Album._types.update(beets.plugins.types(Album))
Item._original_queries = dict(Item._queries)
Album._original_queries = dict(Album._queries)
Item._queries.update(beets.plugins.named_queries(Item))
Album._queries.update(beets.plugins.named_queries(Album))
def unload_plugins(self):
"""Unload all plugins and remove the from the configuration.
"""
@ -237,6 +244,8 @@ class TestHelper(object):
beets.plugins._instances = {}
Item._types = Item._original_types
Album._types = Album._original_types
Item._queries = Item._original_queries
Album._queries = Album._original_queries
def create_importer(self, item_count=1, album_count=1):
"""Create files to import and return corresponding session.

View file

@ -36,6 +36,17 @@ class TestSort(dbcore.query.FieldSort):
pass
class TestQuery(dbcore.query.Query):
def __init__(self, pattern):
self.pattern = pattern
def clause(self):
return None, ()
def match(self):
return True
class TestModel1(dbcore.Model):
_table = 'test'
_flex_table = 'testflex'
@ -49,6 +60,9 @@ class TestModel1(dbcore.Model):
_sorts = {
'some_sort': TestSort,
}
_queries = {
'some_query': TestQuery,
}
@classmethod
def _getters(cls):
@ -519,6 +533,10 @@ class QueryFromStringsTest(unittest.TestCase):
q = self.qfs([''])
self.assertIsInstance(q.subqueries[0], dbcore.query.TrueQuery)
def test_parse_named_query(self):
q = self.qfs(['some_query:foo'])
self.assertIsInstance(q.subqueries[0], TestQuery)
class SortFromStringsTest(unittest.TestCase):
def sfs(self, strings):