Changed the inheritance model to remove types
The inheritance model has changed, we no longer need to store an array of `types` with the model we can just use the classname in `_cls`. See the upgrade docs for information on how to upgrade MongoEngine/mongoengine#148
This commit is contained in:
11
mongoengine/queryset/__init__.py
Normal file
11
mongoengine/queryset/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from mongoengine.errors import (DoesNotExist, MultipleObjectsReturned,
|
||||
InvalidQueryError, OperationError,
|
||||
NotUniqueError)
|
||||
from .field_list import *
|
||||
from .manager import *
|
||||
from .queryset import *
|
||||
from .transform import *
|
||||
from .visitor import *
|
||||
|
||||
__all__ = (field_list.__all__ + manager.__all__ + queryset.__all__ +
|
||||
transform.__all__ + visitor.__all__)
|
||||
51
mongoengine/queryset/field_list.py
Normal file
51
mongoengine/queryset/field_list.py
Normal file
@@ -0,0 +1,51 @@
|
||||
|
||||
__all__ = ('QueryFieldList',)
|
||||
|
||||
|
||||
class QueryFieldList(object):
|
||||
"""Object that handles combinations of .only() and .exclude() calls"""
|
||||
ONLY = 1
|
||||
EXCLUDE = 0
|
||||
|
||||
def __init__(self, fields=[], value=ONLY, always_include=[]):
|
||||
self.value = value
|
||||
self.fields = set(fields)
|
||||
self.always_include = set(always_include)
|
||||
self._id = None
|
||||
|
||||
def __add__(self, f):
|
||||
if not self.fields:
|
||||
self.fields = f.fields
|
||||
self.value = f.value
|
||||
elif self.value is self.ONLY and f.value is self.ONLY:
|
||||
self.fields = self.fields.intersection(f.fields)
|
||||
elif self.value is self.EXCLUDE and f.value is self.EXCLUDE:
|
||||
self.fields = self.fields.union(f.fields)
|
||||
elif self.value is self.ONLY and f.value is self.EXCLUDE:
|
||||
self.fields -= f.fields
|
||||
elif self.value is self.EXCLUDE and f.value is self.ONLY:
|
||||
self.value = self.ONLY
|
||||
self.fields = f.fields - self.fields
|
||||
|
||||
if '_id' in f.fields:
|
||||
self._id = f.value
|
||||
|
||||
if self.always_include:
|
||||
if self.value is self.ONLY and self.fields:
|
||||
self.fields = self.fields.union(self.always_include)
|
||||
else:
|
||||
self.fields -= self.always_include
|
||||
return self
|
||||
|
||||
def __nonzero__(self):
|
||||
return bool(self.fields)
|
||||
|
||||
def as_dict(self):
|
||||
field_list = dict((field, self.value) for field in self.fields)
|
||||
if self._id is not None:
|
||||
field_list['_id'] = self._id
|
||||
return field_list
|
||||
|
||||
def reset(self):
|
||||
self.fields = set([])
|
||||
self.value = self.ONLY
|
||||
61
mongoengine/queryset/manager.py
Normal file
61
mongoengine/queryset/manager.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from functools import partial
|
||||
from .queryset import QuerySet
|
||||
|
||||
__all__ = ('queryset_manager', 'QuerySetManager')
|
||||
|
||||
|
||||
class QuerySetManager(object):
|
||||
"""
|
||||
The default QuerySet Manager.
|
||||
|
||||
Custom QuerySet Manager functions can extend this class and users can
|
||||
add extra queryset functionality. Any custom manager methods must accept a
|
||||
:class:`~mongoengine.Document` class as its first argument, and a
|
||||
:class:`~mongoengine.queryset.QuerySet` as its second argument.
|
||||
|
||||
The method function should return a :class:`~mongoengine.queryset.QuerySet`
|
||||
, probably the same one that was passed in, but modified in some way.
|
||||
"""
|
||||
|
||||
get_queryset = None
|
||||
|
||||
def __init__(self, queryset_func=None):
|
||||
if queryset_func:
|
||||
self.get_queryset = queryset_func
|
||||
self._collections = {}
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
"""Descriptor for instantiating a new QuerySet object when
|
||||
Document.objects is accessed.
|
||||
"""
|
||||
if instance is not None:
|
||||
# Document class being used rather than a document object
|
||||
return self
|
||||
|
||||
# owner is the document that contains the QuerySetManager
|
||||
queryset_class = owner._meta.get('queryset_class') or QuerySet
|
||||
queryset = queryset_class(owner, owner._get_collection())
|
||||
if self.get_queryset:
|
||||
arg_count = self.get_queryset.func_code.co_argcount
|
||||
if arg_count == 1:
|
||||
queryset = self.get_queryset(queryset)
|
||||
elif arg_count == 2:
|
||||
queryset = self.get_queryset(owner, queryset)
|
||||
else:
|
||||
queryset = partial(self.get_queryset, owner, queryset)
|
||||
return queryset
|
||||
|
||||
|
||||
def queryset_manager(func):
|
||||
"""Decorator that allows you to define custom QuerySet managers on
|
||||
:class:`~mongoengine.Document` classes. The manager must be a function that
|
||||
accepts a :class:`~mongoengine.Document` class as its first argument, and a
|
||||
:class:`~mongoengine.queryset.QuerySet` as its second argument. The method
|
||||
function should return a :class:`~mongoengine.queryset.QuerySet`, probably
|
||||
the same one that was passed in, but modified in some way.
|
||||
"""
|
||||
if func.func_code.co_argcount == 1:
|
||||
import warnings
|
||||
msg = 'Methods decorated with queryset_manager should take 2 arguments'
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
return QuerySetManager(func)
|
||||
1257
mongoengine/queryset/queryset.py
Normal file
1257
mongoengine/queryset/queryset.py
Normal file
File diff suppressed because it is too large
Load Diff
237
mongoengine/queryset/transform.py
Normal file
237
mongoengine/queryset/transform.py
Normal file
@@ -0,0 +1,237 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from mongoengine.common import _import_class
|
||||
from mongoengine.errors import InvalidQueryError, LookUpError
|
||||
|
||||
__all__ = ('query', 'update')
|
||||
|
||||
|
||||
COMPARISON_OPERATORS = ('ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod',
|
||||
'all', 'size', 'exists', 'not')
|
||||
GEO_OPERATORS = ('within_distance', 'within_spherical_distance',
|
||||
'within_box', 'within_polygon', 'near', 'near_sphere')
|
||||
STRING_OPERATORS = ('contains', 'icontains', 'startswith',
|
||||
'istartswith', 'endswith', 'iendswith',
|
||||
'exact', 'iexact')
|
||||
CUSTOM_OPERATORS = ('match',)
|
||||
MATCH_OPERATORS = (COMPARISON_OPERATORS + GEO_OPERATORS +
|
||||
STRING_OPERATORS + CUSTOM_OPERATORS)
|
||||
|
||||
UPDATE_OPERATORS = ('set', 'unset', 'inc', 'dec', 'pop', 'push',
|
||||
'push_all', 'pull', 'pull_all', 'add_to_set')
|
||||
|
||||
|
||||
def query(_doc_cls=None, _field_operation=False, **query):
|
||||
"""Transform a query from Django-style format to Mongo format.
|
||||
"""
|
||||
mongo_query = {}
|
||||
merge_query = defaultdict(list)
|
||||
for key, value in query.items():
|
||||
if key == "__raw__":
|
||||
mongo_query.update(value)
|
||||
continue
|
||||
|
||||
parts = key.split('__')
|
||||
indices = [(i, p) for i, p in enumerate(parts) if p.isdigit()]
|
||||
parts = [part for part in parts if not part.isdigit()]
|
||||
# Check for an operator and transform to mongo-style if there is
|
||||
op = None
|
||||
if parts[-1] in MATCH_OPERATORS:
|
||||
op = parts.pop()
|
||||
|
||||
negate = False
|
||||
if parts[-1] == 'not':
|
||||
parts.pop()
|
||||
negate = True
|
||||
|
||||
if _doc_cls:
|
||||
# Switch field names to proper names [set in Field(name='foo')]
|
||||
try:
|
||||
fields = _doc_cls._lookup_field(parts)
|
||||
except Exception, e:
|
||||
raise InvalidQueryError(e)
|
||||
parts = []
|
||||
|
||||
cleaned_fields = []
|
||||
for field in fields:
|
||||
append_field = True
|
||||
if isinstance(field, basestring):
|
||||
parts.append(field)
|
||||
append_field = False
|
||||
else:
|
||||
parts.append(field.db_field)
|
||||
if append_field:
|
||||
cleaned_fields.append(field)
|
||||
|
||||
# Convert value to proper value
|
||||
field = cleaned_fields[-1]
|
||||
|
||||
singular_ops = [None, 'ne', 'gt', 'gte', 'lt', 'lte', 'not']
|
||||
singular_ops += STRING_OPERATORS
|
||||
if op in singular_ops:
|
||||
if isinstance(field, basestring):
|
||||
if (op in STRING_OPERATORS and
|
||||
isinstance(value, basestring)):
|
||||
StringField = _import_class('StringField')
|
||||
value = StringField.prepare_query_value(op, value)
|
||||
else:
|
||||
value = field
|
||||
else:
|
||||
value = field.prepare_query_value(op, value)
|
||||
elif op in ('in', 'nin', 'all', 'near'):
|
||||
# 'in', 'nin' and 'all' require a list of values
|
||||
value = [field.prepare_query_value(op, v) for v in value]
|
||||
|
||||
# if op and op not in COMPARISON_OPERATORS:
|
||||
if op:
|
||||
if op in GEO_OPERATORS:
|
||||
if op == "within_distance":
|
||||
value = {'$within': {'$center': value}}
|
||||
elif op == "within_spherical_distance":
|
||||
value = {'$within': {'$centerSphere': value}}
|
||||
elif op == "within_polygon":
|
||||
value = {'$within': {'$polygon': value}}
|
||||
elif op == "near":
|
||||
value = {'$near': value}
|
||||
elif op == "near_sphere":
|
||||
value = {'$nearSphere': value}
|
||||
elif op == 'within_box':
|
||||
value = {'$within': {'$box': value}}
|
||||
else:
|
||||
raise NotImplementedError("Geo method '%s' has not "
|
||||
"been implemented" % op)
|
||||
elif op in CUSTOM_OPERATORS:
|
||||
if op == 'match':
|
||||
value = {"$elemMatch": value}
|
||||
else:
|
||||
NotImplementedError("Custom method '%s' has not "
|
||||
"been implemented" % op)
|
||||
elif op not in STRING_OPERATORS:
|
||||
value = {'$' + op: value}
|
||||
|
||||
if negate:
|
||||
value = {'$not': value}
|
||||
|
||||
for i, part in indices:
|
||||
parts.insert(i, part)
|
||||
key = '.'.join(parts)
|
||||
if op is None or key not in mongo_query:
|
||||
mongo_query[key] = value
|
||||
elif key in mongo_query:
|
||||
if key in mongo_query and isinstance(mongo_query[key], dict):
|
||||
mongo_query[key].update(value)
|
||||
else:
|
||||
# Store for manually merging later
|
||||
merge_query[key].append(value)
|
||||
|
||||
# The queryset has been filter in such a way we must manually merge
|
||||
for k, v in merge_query.items():
|
||||
merge_query[k].append(mongo_query[k])
|
||||
del mongo_query[k]
|
||||
if isinstance(v, list):
|
||||
value = [{k:val} for val in v]
|
||||
if '$and' in mongo_query.keys():
|
||||
mongo_query['$and'].append(value)
|
||||
else:
|
||||
mongo_query['$and'] = value
|
||||
|
||||
return mongo_query
|
||||
|
||||
|
||||
def update(_doc_cls=None, **update):
|
||||
"""Transform an update spec from Django-style format to Mongo format.
|
||||
"""
|
||||
mongo_update = {}
|
||||
for key, value in update.items():
|
||||
if key == "__raw__":
|
||||
mongo_update.update(value)
|
||||
continue
|
||||
parts = key.split('__')
|
||||
# Check for an operator and transform to mongo-style if there is
|
||||
op = None
|
||||
if parts[0] in UPDATE_OPERATORS:
|
||||
op = parts.pop(0)
|
||||
# Convert Pythonic names to Mongo equivalents
|
||||
if op in ('push_all', 'pull_all'):
|
||||
op = op.replace('_all', 'All')
|
||||
elif op == 'dec':
|
||||
# Support decrement by flipping a positive value's sign
|
||||
# and using 'inc'
|
||||
op = 'inc'
|
||||
if value > 0:
|
||||
value = -value
|
||||
elif op == 'add_to_set':
|
||||
op = op.replace('_to_set', 'ToSet')
|
||||
|
||||
match = None
|
||||
if parts[-1] in COMPARISON_OPERATORS:
|
||||
match = parts.pop()
|
||||
|
||||
if _doc_cls:
|
||||
# Switch field names to proper names [set in Field(name='foo')]
|
||||
try:
|
||||
fields = _doc_cls._lookup_field(parts)
|
||||
except Exception, e:
|
||||
raise InvalidQueryError(e)
|
||||
parts = []
|
||||
|
||||
cleaned_fields = []
|
||||
for field in fields:
|
||||
append_field = True
|
||||
if isinstance(field, basestring):
|
||||
# Convert the S operator to $
|
||||
if field == 'S':
|
||||
field = '$'
|
||||
parts.append(field)
|
||||
append_field = False
|
||||
else:
|
||||
parts.append(field.db_field)
|
||||
if append_field:
|
||||
cleaned_fields.append(field)
|
||||
|
||||
# Convert value to proper value
|
||||
field = cleaned_fields[-1]
|
||||
|
||||
if op in (None, 'set', 'push', 'pull'):
|
||||
if field.required or value is not None:
|
||||
value = field.prepare_query_value(op, value)
|
||||
elif op in ('pushAll', 'pullAll'):
|
||||
value = [field.prepare_query_value(op, v) for v in value]
|
||||
elif op == 'addToSet':
|
||||
if isinstance(value, (list, tuple, set)):
|
||||
value = [field.prepare_query_value(op, v) for v in value]
|
||||
elif field.required or value is not None:
|
||||
value = field.prepare_query_value(op, value)
|
||||
|
||||
if match:
|
||||
match = '$' + match
|
||||
value = {match: value}
|
||||
|
||||
key = '.'.join(parts)
|
||||
|
||||
if not op:
|
||||
raise InvalidQueryError("Updates must supply an operation "
|
||||
"eg: set__FIELD=value")
|
||||
|
||||
if 'pull' in op and '.' in key:
|
||||
# Dot operators don't work on pull operations
|
||||
# it uses nested dict syntax
|
||||
if op == 'pullAll':
|
||||
raise InvalidQueryError("pullAll operations only support "
|
||||
"a single field depth")
|
||||
|
||||
parts.reverse()
|
||||
for key in parts:
|
||||
value = {key: value}
|
||||
elif op == 'addToSet' and isinstance(value, list):
|
||||
value = {key: {"$each": value}}
|
||||
else:
|
||||
value = {key: value}
|
||||
key = '$' + op
|
||||
|
||||
if key not in mongo_update:
|
||||
mongo_update[key] = value
|
||||
elif key in mongo_update and isinstance(mongo_update[key], dict):
|
||||
mongo_update[key].update(value)
|
||||
|
||||
return mongo_update
|
||||
237
mongoengine/queryset/visitor.py
Normal file
237
mongoengine/queryset/visitor.py
Normal file
@@ -0,0 +1,237 @@
|
||||
import copy
|
||||
|
||||
from mongoengine.errors import InvalidQueryError
|
||||
from mongoengine.python_support import product, reduce
|
||||
|
||||
from mongoengine.queryset import transform
|
||||
|
||||
__all__ = ('Q',)
|
||||
|
||||
|
||||
class QNodeVisitor(object):
|
||||
"""Base visitor class for visiting Q-object nodes in a query tree.
|
||||
"""
|
||||
|
||||
def visit_combination(self, combination):
|
||||
"""Called by QCombination objects.
|
||||
"""
|
||||
return combination
|
||||
|
||||
def visit_query(self, query):
|
||||
"""Called by (New)Q objects.
|
||||
"""
|
||||
return query
|
||||
|
||||
|
||||
class SimplificationVisitor(QNodeVisitor):
|
||||
"""Simplifies query trees by combinging unnecessary 'and' connection nodes
|
||||
into a single Q-object.
|
||||
"""
|
||||
|
||||
def visit_combination(self, combination):
|
||||
if combination.operation == combination.AND:
|
||||
# The simplification only applies to 'simple' queries
|
||||
if all(isinstance(node, Q) for node in combination.children):
|
||||
queries = [node.query for node in combination.children]
|
||||
return Q(**self._query_conjunction(queries))
|
||||
return combination
|
||||
|
||||
def _query_conjunction(self, queries):
|
||||
"""Merges query dicts - effectively &ing them together.
|
||||
"""
|
||||
query_ops = set()
|
||||
combined_query = {}
|
||||
for query in queries:
|
||||
ops = set(query.keys())
|
||||
# Make sure that the same operation isn't applied more than once
|
||||
# to a single field
|
||||
intersection = ops.intersection(query_ops)
|
||||
if intersection:
|
||||
msg = 'Duplicate query conditions: '
|
||||
raise InvalidQueryError(msg + ', '.join(intersection))
|
||||
|
||||
query_ops.update(ops)
|
||||
combined_query.update(copy.deepcopy(query))
|
||||
return combined_query
|
||||
|
||||
|
||||
class QueryTreeTransformerVisitor(QNodeVisitor):
|
||||
"""Transforms the query tree in to a form that may be used with MongoDB.
|
||||
"""
|
||||
|
||||
def visit_combination(self, combination):
|
||||
if combination.operation == combination.AND:
|
||||
# MongoDB doesn't allow us to have too many $or operations in our
|
||||
# queries, so the aim is to move the ORs up the tree to one
|
||||
# 'master' $or. Firstly, we must find all the necessary parts (part
|
||||
# of an AND combination or just standard Q object), and store them
|
||||
# separately from the OR parts.
|
||||
or_groups = []
|
||||
and_parts = []
|
||||
for node in combination.children:
|
||||
if isinstance(node, QCombination):
|
||||
if node.operation == node.OR:
|
||||
# Any of the children in an $or component may cause
|
||||
# the query to succeed
|
||||
or_groups.append(node.children)
|
||||
elif node.operation == node.AND:
|
||||
and_parts.append(node)
|
||||
elif isinstance(node, Q):
|
||||
and_parts.append(node)
|
||||
|
||||
# Now we combine the parts into a usable query. AND together all of
|
||||
# the necessary parts. Then for each $or part, create a new query
|
||||
# that ANDs the necessary part with the $or part.
|
||||
clauses = []
|
||||
for or_group in product(*or_groups):
|
||||
q_object = reduce(lambda a, b: a & b, and_parts, Q())
|
||||
q_object = reduce(lambda a, b: a & b, or_group, q_object)
|
||||
clauses.append(q_object)
|
||||
# Finally, $or the generated clauses in to one query. Each of the
|
||||
# clauses is sufficient for the query to succeed.
|
||||
return reduce(lambda a, b: a | b, clauses, Q())
|
||||
|
||||
if combination.operation == combination.OR:
|
||||
children = []
|
||||
# Crush any nested ORs in to this combination as MongoDB doesn't
|
||||
# support nested $or operations
|
||||
for node in combination.children:
|
||||
if (isinstance(node, QCombination) and
|
||||
node.operation == combination.OR):
|
||||
children += node.children
|
||||
else:
|
||||
children.append(node)
|
||||
combination.children = children
|
||||
|
||||
return combination
|
||||
|
||||
|
||||
class QueryCompilerVisitor(QNodeVisitor):
|
||||
"""Compiles the nodes in a query tree to a PyMongo-compatible query
|
||||
dictionary.
|
||||
"""
|
||||
|
||||
def __init__(self, document):
|
||||
self.document = document
|
||||
|
||||
def visit_combination(self, combination):
|
||||
if combination.operation == combination.OR:
|
||||
return {'$or': combination.children}
|
||||
elif combination.operation == combination.AND:
|
||||
return self._mongo_query_conjunction(combination.children)
|
||||
return combination
|
||||
|
||||
def visit_query(self, query):
|
||||
return transform.query(self.document, **query.query)
|
||||
|
||||
def _mongo_query_conjunction(self, queries):
|
||||
"""Merges Mongo query dicts - effectively &ing them together.
|
||||
"""
|
||||
combined_query = {}
|
||||
for query in queries:
|
||||
for field, ops in query.items():
|
||||
if field not in combined_query:
|
||||
combined_query[field] = ops
|
||||
else:
|
||||
# The field is already present in the query the only way
|
||||
# we can merge is if both the existing value and the new
|
||||
# value are operation dicts, reject anything else
|
||||
if (not isinstance(combined_query[field], dict) or
|
||||
not isinstance(ops, dict)):
|
||||
message = 'Conflicting values for ' + field
|
||||
raise InvalidQueryError(message)
|
||||
|
||||
current_ops = set(combined_query[field].keys())
|
||||
new_ops = set(ops.keys())
|
||||
# Make sure that the same operation isn't applied more than
|
||||
# once to a single field
|
||||
intersection = current_ops.intersection(new_ops)
|
||||
if intersection:
|
||||
msg = 'Duplicate query conditions: '
|
||||
raise InvalidQueryError(msg + ', '.join(intersection))
|
||||
|
||||
# Right! We've got two non-overlapping dicts of operations!
|
||||
combined_query[field].update(copy.deepcopy(ops))
|
||||
return combined_query
|
||||
|
||||
|
||||
class QNode(object):
|
||||
"""Base class for nodes in query trees.
|
||||
"""
|
||||
|
||||
AND = 0
|
||||
OR = 1
|
||||
|
||||
def to_query(self, document):
|
||||
query = self.accept(SimplificationVisitor())
|
||||
query = query.accept(QueryTreeTransformerVisitor())
|
||||
query = query.accept(QueryCompilerVisitor(document))
|
||||
return query
|
||||
|
||||
def accept(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
def _combine(self, other, operation):
|
||||
"""Combine this node with another node into a QCombination object.
|
||||
"""
|
||||
if getattr(other, 'empty', True):
|
||||
return self
|
||||
|
||||
if self.empty:
|
||||
return other
|
||||
|
||||
return QCombination(operation, [self, other])
|
||||
|
||||
@property
|
||||
def empty(self):
|
||||
return False
|
||||
|
||||
def __or__(self, other):
|
||||
return self._combine(other, self.OR)
|
||||
|
||||
def __and__(self, other):
|
||||
return self._combine(other, self.AND)
|
||||
|
||||
|
||||
class QCombination(QNode):
|
||||
"""Represents the combination of several conditions by a given logical
|
||||
operator.
|
||||
"""
|
||||
|
||||
def __init__(self, operation, children):
|
||||
self.operation = operation
|
||||
self.children = []
|
||||
for node in children:
|
||||
# If the child is a combination of the same type, we can merge its
|
||||
# children directly into this combinations children
|
||||
if isinstance(node, QCombination) and node.operation == operation:
|
||||
self.children += node.children
|
||||
else:
|
||||
self.children.append(node)
|
||||
|
||||
def accept(self, visitor):
|
||||
for i in range(len(self.children)):
|
||||
if isinstance(self.children[i], QNode):
|
||||
self.children[i] = self.children[i].accept(visitor)
|
||||
|
||||
return visitor.visit_combination(self)
|
||||
|
||||
@property
|
||||
def empty(self):
|
||||
return not bool(self.children)
|
||||
|
||||
|
||||
class Q(QNode):
|
||||
"""A simple query object, used in a query tree to build up more complex
|
||||
query structures.
|
||||
"""
|
||||
|
||||
def __init__(self, **query):
|
||||
self.query = query
|
||||
|
||||
def accept(self, visitor):
|
||||
return visitor.visit_query(self)
|
||||
|
||||
@property
|
||||
def empty(self):
|
||||
return not bool(self.query)
|
||||
Reference in New Issue
Block a user