mirror of
https://github.com/beetbox/beets.git
synced 2025-12-30 12:32:33 +01:00
A bunch of docs on query parsing (fix #1794)
This commit is contained in:
parent
c54753d5d2
commit
a218da1414
1 changed files with 60 additions and 29 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue