diff --git a/beets/library.py b/beets/library.py index b3b0eb760..8bef98d84 100644 --- a/beets/library.py +++ b/beets/library.py @@ -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 diff --git a/beets/plugins.py b/beets/plugins.py index 3749a464b..5a5a718f7 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -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()) diff --git a/beetsplug/fuzzy.py b/beetsplug/fuzzy.py index 4362cbb9f..2186499cf 100644 --- a/beetsplug/fuzzy.py +++ b/beetsplug/fuzzy.py @@ -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 diff --git a/docs/changelog.rst b/docs/changelog.rst index ede4f82b1..b00876395 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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) ------------------------- diff --git a/docs/plugins/fuzzy.rst b/docs/plugins/fuzzy.rst index 604b0d998..3f4115168 100644 --- a/docs/plugins/fuzzy.rst +++ b/docs/plugins/fuzzy.rst @@ -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 diff --git a/docs/plugins/writing.rst b/docs/plugins/writing.rst index 022ed1688..0e8183763 100644 --- a/docs/plugins/writing.rst +++ b/docs/plugins/writing.rst @@ -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 +` 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 } -