some low-level tweaks to extensible queries (#214)

This commit is contained in:
Adrian Sampson 2013-03-13 21:59:03 -07:00
parent 292092bef7
commit 40b49ac786
6 changed files with 61 additions and 78 deletions

View file

@ -17,7 +17,6 @@
import sqlite3
import os
import re
import difflib
import sys
import logging
import shlex
@ -515,7 +514,6 @@ class RegexpQuery(FieldQuery):
value = util.as_string(getattr(item, self.field))
return self.regexp.search(value) is not None
class PluginQuery(FieldQuery):
"""The base class to add queries using beets plugins. Plugins can add
special queries by defining a subclass of PluginQuery and overriding
@ -525,9 +523,18 @@ class PluginQuery(FieldQuery):
super(PluginQuery, self).__init__(field, pattern)
def clause(self):
clause = "{name}(?, {field})".format(name=self.__class__.__name__, field=self.field)
# Invoke the registered SQLite function.
clause = "{name}(?, {field})".format(name=self.__class__.__name__,
field=self.field)
return clause, [self.pattern]
@classmethod
def register(cls, conn):
"""Register this query's matching function with the SQLite
connection.
"""
conn.create_function(cls.__name__, 2, cls(None, None).match)
class BooleanQuery(MatchQuery):
"""Matches a boolean field. Pattern should either be a boolean or a
string reflecting a boolean.
@ -593,11 +600,14 @@ class CollectionQuery(Query):
@classmethod
def _parse_query_part(cls, part):
"""Takes a query in the form of a key/value pair separated by a
colon. The value part is matched against a list of prefixes that can be
extended by plugins to add custom query types. For example, the colon
prefix denotes a regular exporession query.
colon. The value part is matched against a list of prefixes that
can be extended by plugins to add custom query types. For
example, the colon prefix denotes a regular expression query.
The function returns a tuple of(key, value, Query)
The function returns a tuple of `(key, value, cls)`. `key` may
be None, indicating that any field may be matched. `cls` is
either a subclass of `PluginQuery` or `None` indicating a
"normal" query.
For instance,
parse_query('stapler') == (None, 'stapler', None)
@ -611,17 +621,17 @@ class CollectionQuery(Query):
part = part.strip()
match = cls._pq_regex.match(part)
cls.prefixes = {':': RegexpQuery}
cls.prefixes.update(plugins.queries())
prefixes = {':': RegexpQuery}
prefixes.update(plugins.queries())
if match:
key = match.group(1)
term = match.group(2).replace('\:', ':')
# match the search term against the list of prefixes
for pre, query in cls.prefixes.items():
# Match the search term against the list of prefixes.
for pre, query_class in prefixes.items():
if term.startswith(pre):
return (key, term[len(pre):], query)
return (key, term, None) # None means a normal query
return key, term[len(pre):], query_class
return key, term, None # None means a normal query.
@classmethod
def from_strings(cls, query_parts, default_fields=None,
@ -637,7 +647,7 @@ class CollectionQuery(Query):
if not res:
continue
key, pattern, prefix_query = res
key, pattern, query_class = res
# No key specified.
if key is None:
@ -646,8 +656,9 @@ class CollectionQuery(Query):
subqueries.append(PathQuery(pattern))
else:
# Match any field.
if prefix_query:
subq = AnyPluginQuery(pattern, default_fields, cls=prefix_query)
if query_class:
subq = AnyPluginQuery(pattern, default_fields,
cls=query_class)
else:
subq = AnySubstringQuery(pattern, default_fields)
subqueries.append(subq)
@ -662,8 +673,8 @@ class CollectionQuery(Query):
# Other (recognized) field.
elif key.lower() in all_keys:
if prefix_query is not None:
subqueries.append(prefix_query(key.lower(), pattern))
if query_class:
subqueries.append(query_class(key.lower(), pattern))
else:
subqueries.append(SubstringQuery(key.lower(), pattern))
@ -724,42 +735,10 @@ class AnySubstringQuery(CollectionQuery):
return True
return False
class AnyRegexpQuery(CollectionQuery):
"""A query that matches a regexp in any of a list of metadata
fields.
"""
def __init__(self, pattern, fields=None):
"""Create a query for regexp over the sequence of fields
given. If no fields are given, all available fields are
used.
"""
self.regexp = re.compile(pattern)
self.fields = fields or ITEM_KEYS_WRITABLE
subqueries = []
for field in self.fields:
subqueries.append(RegexpQuery(field, pattern))
super(AnyRegexpQuery, self).__init__(subqueries)
def clause(self):
return self.clause_with_joiner('or')
def match(self, item):
for fld in self.fields:
try:
val = getattr(item, fld)
except KeyError:
continue
if isinstance(val, basestring) and \
self.regexp.match(val) is not None:
return True
return False
class AnyPluginQuery(CollectionQuery):
"""A query that dispatch the matching function to the match method of
the cls provided to the contstructor using a list of metadata fields.
"""
def __init__(self, pattern, fields=None, cls=PluginQuery):
subqueries = []
self.pattern = pattern
@ -1174,9 +1153,9 @@ class Library(BaseLibrary):
# Add the REGEXP function to SQLite queries.
conn.create_function("REGEXP", 2, _regexp)
# Register plugin queries
for prefix, query in plugins.queries().items():
conn.create_function(query.__name__, 2, query(None, None).match)
# Register plugin queries.
for prefix, query_class in plugins.queries().items():
query_class.register(conn)
self._connections[thread_id] = conn
return conn

View file

@ -56,7 +56,9 @@ class BeetsPlugin(object):
return ()
def queries(self):
"""Should return a dict of {prefix : beets.library.PluginQuery}"""
"""Should return a dict mapping prefixes to PluginQuery
subclasses.
"""
return {}
def track_distance(self, item, info):
@ -215,8 +217,9 @@ def commands():
return out
def queries():
"""Returns a dict of {prefix: beet.library.PluginQuery} objects from all
loaded plugins. """
"""Returns a dict mapping prefix strings to beet.library.PluginQuery
subclasses all loaded plugins.
"""
out = {}
for plugin in find_plugins():
out.update(plugin.queries())

View file

@ -12,12 +12,11 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Like beet list, but with fuzzy matching
"""Provides a fuzzy matching query.
"""
from beets.plugins import BeetsPlugin
from beets.library import PluginQuery
from beets.ui import Subcommand, decargs, print_obj
from beets import util
import beets
from beets.util import confit
@ -28,7 +27,7 @@ class FuzzyQuery(PluginQuery):
def __init__(self, field, pattern):
super(FuzzyQuery, self).__init__(field, pattern)
try:
self.threshold = beets.config['fuzzy']['threshold'].as_number()
self.threshold = beets.config['fuzzy']['threshold'].as_number()
except confit.NotFoundError:
self.threshold = 0.7
@ -37,7 +36,7 @@ class FuzzyQuery(PluginQuery):
return False
val = util.as_string(val)
# smartcase
if(pattern.islower()):
if pattern.islower():
val = val.lower()
queryMatcher = difflib.SequenceMatcher(None, pattern, val)
return queryMatcher.quick_ratio() >= self.threshold

View file

@ -23,6 +23,12 @@ Other stuff:
track in MusicBrainz and updates your library to reflect it. This can help
you easily correct errors that have been fixed in the MB database. Thanks to
Jakob Schnitzer.
* :doc:`/plugins/fuzzy`: The ``fuzzy`` command was removed and replaced with a
new query type. To perform fuzzy searches, use the ``~`` prefix with
:ref:`list-cmd` or other commands. Thanks to Philippe Mongeau.
* As part of the above, plugins can now extend the query syntax and new kinds
of matching capabilities to beets. See :ref:`extend-query`. Thanks again to
Philippe Mongeau.
* :doc:`/plugins/convert`: A new ``--keep-new`` option lets you store
transcoded files in your library while backing up the originals (instead of
vice-versa). Thanks to Lucas Duailibe.
@ -59,8 +65,6 @@ Other stuff:
the Echo Nest library.
* :doc:`/plugins/chroma`: Catch Acoustid Web service errors when submitting
fingerprints.
* :ref:`extend-query`: Plugins can now extend the query syntax. Thanks to
Philippe Mongeau
1.1b2 (February 16, 2013)
-------------------------

View file

@ -1,8 +1,8 @@
Fuzzy Search Plugin
===================
The ``fuzzy`` plugin provides a query prefix that search you library using fuzzy
pattern matching. This can be useful if you want to find a track with
The ``fuzzy`` plugin provides a prefixed query that search you library using
fuzzy pattern matching. This can be useful if you want to find a track with
complicated characters in the title.
First, enable the plugin named ``fuzzy`` (see :doc:`/plugins/index`).
@ -11,14 +11,14 @@ You'll then be able to use the ``~`` prefix to use fuzzy matching::
$ beet ls '~Vareoldur'
Sigur Rós - Valtari - Varðeldur
The plugin provides to config option to let you choose the prefix and the
The plugin provides config options that let you choose the prefix and the
threshold.::
fuzzy:
threshold: 0.8
prefix: '@'
A threshold value of ``1`` will show only perfect matches and a value of ``0``
A threshold value of 1.0 will show only perfect matches and a value of 0.0
will match everything.
The default prefix ``~`` needs to be escaped or quoted in most shells. If this

View file

@ -327,32 +327,30 @@ to register it::
.. _extend-query:
Extend the Query Syntax
^^^^^^^^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^^^^
Beets already support searching using regular expressions by prepending search
terms with the colon prefix. It is possible to add new prefix by extending the
``PluginQuery`` class.
You can add new kinds of queries to beets' :doc:`query syntax
</reference/query>` indicated by a prefix. As an example, beets already
supports regular expression queries, which are indicated by a colon
prefix---plugins can do the same.
The plugin then need to declare its new queries by returning a ``dict`` of
``{prefix: PluginQuery}`` from the ``queries`` method.
To do so, define a subclass of the ``PluginQuery`` type from the
``beets.library`` module. Then, in the ``queries`` method of your plugin
class, return a dictionary mapping prefix strings to query classes.
The following example plugins declares a query using the ``@`` prefix. So the
plugin will be called if we issue a command like ``beet ls @something`` or
``beet ls artist:@something``::
from beets.plugins import BeetsPlugin
from beets.Library import PluginQuery
from beets.library import PluginQuery
class ExampleQuery(PluginQuery):
def match(self, pattern, val):
return True # this will simply match everything
return True # This will just match everything.
class ExamplePlugin(BeetsPlugin):
def queries():
# plugins need to declare theire queries by
# returning a dict of {prefix: PluginQuery}
# from the queries() function
return {
'@': ExampleQuery
}