mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
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:
commit
9320db21eb
9 changed files with 93 additions and 34 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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__()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
--------------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue