v0.166.0
  1"""
  2A class for storing a tree graph. Primarily used for filter constructs in the
  3ORM.
  4"""
  5
  6import copy
  7from typing import TYPE_CHECKING, Any
  8
  9if TYPE_CHECKING:
 10    from typing import Self
 11
 12from plain.utils.hashable import make_hashable
 13
 14
 15class Node:
 16    """
 17    A single internal node in the tree graph. A Node should be viewed as a
 18    connection (the root) with the children being either leaf nodes or other
 19    Node instances.
 20    """
 21
 22    # Standard connector type. Clients usually won't use this at all and
 23    # subclasses will usually override the value.
 24    default: str = "DEFAULT"
 25
 26    def __init__(
 27        self,
 28        children: list[Any] | None = None,
 29        connector: str | None = None,
 30        negated: bool = False,
 31    ) -> None:
 32        """Construct a new Node. If no connector is given, use the default."""
 33        self.children: list[Any] = children[:] if children else []
 34        self.connector: str = connector or self.default
 35        self.negated: bool = negated
 36
 37    @classmethod
 38    def create(
 39        cls,
 40        children: list[Any] | None = None,
 41        connector: str | None = None,
 42        negated: bool = False,
 43    ) -> Self:
 44        """
 45        Create a new instance using Node() instead of __init__() as some
 46        subclasses, e.g. plain.postgres.query_utils.Q, may implement a custom
 47        __init__() with a signature that conflicts with the one defined in
 48        Node.__init__().
 49        """
 50        obj = Node(children, connector or cls.default, negated)
 51        obj.__class__ = cls
 52        return obj  # ty: ignore[invalid-return-type]
 53
 54    def __str__(self) -> str:
 55        template = "(NOT (%s: %s))" if self.negated else "(%s: %s)"
 56        return template % (self.connector, ", ".join(str(c) for c in self.children))
 57
 58    def __repr__(self) -> str:
 59        return f"<{self.__class__.__name__}: {self}>"
 60
 61    def __copy__(self) -> Self:
 62        obj = self.create(connector=self.connector, negated=self.negated)
 63        obj.children = self.children  # Don't [:] as .__init__() via .create() does.
 64        return obj
 65
 66    copy = __copy__
 67
 68    def __deepcopy__(self, memodict: dict[int, Any]) -> Self:
 69        obj = self.create(connector=self.connector, negated=self.negated)
 70        obj.children = copy.deepcopy(self.children, memodict)
 71        return obj
 72
 73    def __len__(self) -> int:
 74        """Return the number of children this node has."""
 75        return len(self.children)
 76
 77    def __bool__(self) -> bool:
 78        """Return whether or not this node has children."""
 79        return bool(self.children)
 80
 81    def __contains__(self, other: Any) -> bool:
 82        """Return True if 'other' is a direct child of this instance."""
 83        return other in self.children
 84
 85    def __eq__(self, other: object) -> bool:
 86        return (
 87            isinstance(other, Node)
 88            and self.__class__ == other.__class__
 89            and self.connector == other.connector
 90            and self.negated == other.negated
 91            and self.children == other.children
 92        )
 93
 94    def __hash__(self) -> int:
 95        return hash(
 96            (
 97                self.__class__,
 98                self.connector,
 99                self.negated,
100                *make_hashable(self.children),
101            )
102        )
103
104    def add(self, data: Any, conn_type: str) -> Any:
105        """
106        Combine this tree and the data represented by data using the
107        connector conn_type. The combine is done by squashing the node other
108        away if possible.
109
110        This tree (self) will never be pushed to a child node of the
111        combined tree, nor will the connector or negated properties change.
112
113        Return a node which can be used in place of data regardless if the
114        node other got squashed or not.
115        """
116        if self.connector != conn_type:
117            obj = self.copy()
118            self.connector = conn_type
119            self.children = [obj, data]
120            return data
121        elif (
122            isinstance(data, Node)
123            and not data.negated
124            and (data.connector == conn_type or len(data) == 1)
125        ):
126            # We can squash the other node's children directly into this node.
127            # We are just doing (AB)(CD) == (ABCD) here, with the addition that
128            # if the length of the other node is 1 the connector doesn't
129            # matter. However, for the len(self) == 1 case we don't want to do
130            # the squashing, as it would alter self.connector.
131            self.children.extend(data.children)
132            return self
133        else:
134            # We could use perhaps additional logic here to see if some
135            # children could be used for pushdown here.
136            self.children.append(data)
137            return data
138
139    def negate(self) -> None:
140        """Negate the sense of the root connector."""
141        self.negated = not self.negated