diff --git a/beets/dbcore/queryparse.py b/beets/dbcore/queryparse.py index b421ebf40..fbc4626f5 100644 --- a/beets/dbcore/queryparse.py +++ b/beets/dbcore/queryparse.py @@ -39,32 +39,49 @@ PARSE_QUERY_PART_REGEX = re.compile( def parse_query_part(part, query_classes={}, prefixes={}, default_class=query.SubstringQuery): - """Take a query in the form of a key/value pair separated by a - colon and return a tuple of `(key, value, cls, negate)`. `key` may be None, - indicating that any field may be matched. `cls` is a subclass of - `FieldQuery`. `negate` is a boolean indicating if the query is negated. + """Parse a single *query part*, which is a chunk of a complete query + string representing a single criterion. - The optional `query_classes` parameter maps field names to default - query types; `default_class` is the fallback. `prefixes` is a map - from query prefix markers and query types. Prefix-indicated queries - take precedence over type-based queries. + A query part is a string consisting of: + - A *pattern*: the value to look for. + - Optionally, a *field name* preceding the pattern, separated by a + colon. So in `foo:bar`, `foo` is the field name and `bar` is the + pattern. + - Optionally, a *query prefix* just before the field (and after the + optional colon) indicating the type of query that should be used. For + example, in `~foo`, `~` might be a prefix. (The set of prefixes to + look for is given in the `prefixes` parameter.) + - Optionally, a negation indicator, `-` or `^`, at the very beginning. - To determine the query class, two factors are used: prefixes and - field types. For example, the colon prefix denotes a regular - expression query and a type map might provide a special kind of - query for numeric values. If neither a prefix nor a specific query - class is available, `default_class` is used. + Both prefixes and the separating `:` character may be escaped with a + backslash to avoid their normal meaning. - For instance, - 'stapler' -> (None, 'stapler', SubstringQuery, False) - 'color:red' -> ('color', 'red', SubstringQuery, False) - ':^Quiet' -> (None, '^Quiet', RegexpQuery, False) - 'color::b..e' -> ('color', 'b..e', RegexpQuery, False) - '-color:red' -> ('color', 'red', SubstringQuery, True) + The function returns a tuple consisting of: + - The field name: a string or None if it's not present. + - The pattern, a string. + - The query class to use, which inherits from the base + :class:`Query` type. + - A negation flag, a bool. - Prefixes may be "escaped" with a backslash to disable the keying - behavior. + The three optional parameters determine which query class is used (i.e., + the third return value). They are: + - `query_classes`, which maps field names to query classes. These + are used when no explicit prefix is present. + - `prefixes`, which maps prefix strings to query classes. + - `default_class`, the fallback when neither the field nor a prefix + indicates a query class. + + So the precedence for determining which query class to return is: + prefix, followed by field, and finally the default. + + For example, assuming the `:` prefix is used for `RegexpQuery`: + - `'stapler'` -> `(None, 'stapler', SubstringQuery, False)` + - `'color:red'` -> `('color', 'red', SubstringQuery, False)` + - `':^Quiet'` -> `(None, '^Quiet', RegexpQuery, False)` + - `'color::b..e'` -> `('color', 'b..e', RegexpQuery, False)` + - `'-color:red'` -> `('color', 'red', SubstringQuery, True)` """ + # Apply the regular expression and extract the components. part = part.strip() match = PARSE_QUERY_PART_REGEX.match(part) @@ -73,25 +90,36 @@ def parse_query_part(part, query_classes={}, prefixes={}, key = match.group(2) term = match.group(3).replace('\:', ':') - # Match the search term against the list of prefixes. + # Check whether there's a prefix in the query and use the + # corresponding query type. for pre, query_class in prefixes.items(): if term.startswith(pre): return key, term[len(pre):], query_class, negate - # No matching prefix: use type-based or fallback/default query. + # No matching prefix, so use either the query class determined by + # the field or the default as a fallback. query_class = query_classes.get(key, default_class) return key, term, query_class, negate def construct_query_part(model_cls, prefixes, query_part): - """Create a query from a single query component, `query_part`, for - querying instances of `model_cls`. Return a `Query` instance. + """Parse a *query part* string and return a :class:`Query` object. + + :param model_cls: The :class:`Model` class that this is a query for. + This is used to determine the appropriate query types for the + model's fields. + :param prefixes: A map from prefix strings to :class:`Query` types. + :param query_part: The string to parse. + + See the documentation for `parse_query_part` for more information on + query part syntax. """ - # Shortcut for empty query parts. + # A shortcut for empty query parts. if not query_part: return query.TrueQuery() - # Get the query classes for each possible field. + # Use `model_cls` to build up a map from field names to `Query` + # classes. query_classes = {} for k, t in itertools.chain(model_cls._fields.items(), model_cls._types.items()): @@ -101,7 +129,8 @@ def construct_query_part(model_cls, prefixes, query_part): key, pattern, query_class, negate = \ parse_query_part(query_part, query_classes, prefixes) - # No key specified. + # If there's no key (field name) specified, this is a "match + # anything" query. if key is None: if issubclass(query_class, query.FieldQuery): # The query type matches a specific field, but none was @@ -114,12 +143,14 @@ def construct_query_part(model_cls, prefixes, query_part): else: return q else: - # Other query type. + # Non-field query type. if negate: return query.NotQuery(query_class(pattern)) else: return 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) if negate: