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