mirror of
https://github.com/django/django.git
synced 2025-04-25 09:44:36 +00:00
Address a long standing bug in a Where.add optimization to discard equal nodes that was surfaced by implementing equality for Lookup instances in bbf141bcdc31f1324048af9233583a523ac54c94. Thanks Shaheed Haque for the report.
125 lines
4.8 KiB
Python
125 lines
4.8 KiB
Python
"""
|
|
A class for storing a tree graph. Primarily used for filter constructs in the
|
|
ORM.
|
|
"""
|
|
|
|
import copy
|
|
|
|
from django.utils.hashable import make_hashable
|
|
|
|
|
|
class Node:
|
|
"""
|
|
A single internal node in the tree graph. A Node should be viewed as a
|
|
connection (the root) with the children being either leaf nodes or other
|
|
Node instances.
|
|
"""
|
|
# Standard connector type. Clients usually won't use this at all and
|
|
# subclasses will usually override the value.
|
|
default = 'DEFAULT'
|
|
|
|
def __init__(self, children=None, connector=None, negated=False):
|
|
"""Construct a new Node. If no connector is given, use the default."""
|
|
self.children = children[:] if children else []
|
|
self.connector = connector or self.default
|
|
self.negated = negated
|
|
|
|
# Required because django.db.models.query_utils.Q. Q. __init__() is
|
|
# problematic, but it is a natural Node subclass in all other respects.
|
|
@classmethod
|
|
def _new_instance(cls, children=None, connector=None, negated=False):
|
|
"""
|
|
Create a new instance of this class when new Nodes (or subclasses) are
|
|
needed in the internal code in this class. Normally, it just shadows
|
|
__init__(). However, subclasses with an __init__ signature that aren't
|
|
an extension of Node.__init__ might need to implement this method to
|
|
allow a Node to create a new instance of them (if they have any extra
|
|
setting up to do).
|
|
"""
|
|
obj = Node(children, connector, negated)
|
|
obj.__class__ = cls
|
|
return obj
|
|
|
|
def __str__(self):
|
|
template = '(NOT (%s: %s))' if self.negated else '(%s: %s)'
|
|
return template % (self.connector, ', '.join(str(c) for c in self.children))
|
|
|
|
def __repr__(self):
|
|
return "<%s: %s>" % (self.__class__.__name__, self)
|
|
|
|
def __deepcopy__(self, memodict):
|
|
obj = Node(connector=self.connector, negated=self.negated)
|
|
obj.__class__ = self.__class__
|
|
obj.children = copy.deepcopy(self.children, memodict)
|
|
return obj
|
|
|
|
def __len__(self):
|
|
"""Return the number of children this node has."""
|
|
return len(self.children)
|
|
|
|
def __bool__(self):
|
|
"""Return whether or not this node has children."""
|
|
return bool(self.children)
|
|
|
|
def __contains__(self, other):
|
|
"""Return True if 'other' is a direct child of this instance."""
|
|
return other in self.children
|
|
|
|
def __eq__(self, other):
|
|
return (
|
|
self.__class__ == other.__class__ and
|
|
(self.connector, self.negated) == (other.connector, other.negated) and
|
|
self.children == other.children
|
|
)
|
|
|
|
def __hash__(self):
|
|
return hash((self.__class__, self.connector, self.negated, *make_hashable(self.children)))
|
|
|
|
def add(self, data, conn_type, squash=True):
|
|
"""
|
|
Combine this tree and the data represented by data using the
|
|
connector conn_type. The combine is done by squashing the node other
|
|
away if possible.
|
|
|
|
This tree (self) will never be pushed to a child node of the
|
|
combined tree, nor will the connector or negated properties change.
|
|
|
|
Return a node which can be used in place of data regardless if the
|
|
node other got squashed or not.
|
|
|
|
If `squash` is False the data is prepared and added as a child to
|
|
this tree without further logic.
|
|
"""
|
|
if self.connector == conn_type and data in self.children:
|
|
return data
|
|
if not squash:
|
|
self.children.append(data)
|
|
return data
|
|
if self.connector == conn_type:
|
|
# We can reuse self.children to append or squash the node other.
|
|
if (isinstance(data, Node) and not data.negated and
|
|
(data.connector == conn_type or len(data) == 1)):
|
|
# We can squash the other node's children directly into this
|
|
# node. We are just doing (AB)(CD) == (ABCD) here, with the
|
|
# addition that if the length of the other node is 1 the
|
|
# connector doesn't matter. However, for the len(self) == 1
|
|
# case we don't want to do the squashing, as it would alter
|
|
# self.connector.
|
|
self.children.extend(data.children)
|
|
return self
|
|
else:
|
|
# We could use perhaps additional logic here to see if some
|
|
# children could be used for pushdown here.
|
|
self.children.append(data)
|
|
return data
|
|
else:
|
|
obj = self._new_instance(self.children, self.connector,
|
|
self.negated)
|
|
self.connector = conn_type
|
|
self.children = [obj, data]
|
|
return data
|
|
|
|
def negate(self):
|
|
"""Negate the sense of the root connector."""
|
|
self.negated = not self.negated
|