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