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