# copyright 2004-2021 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of rql.
#
# rql is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# rql is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with rql. If not, see <http://www.gnu.org/licenses/>.
"""Base classes for RQL syntax tree nodes.
Note: this module uses __slots__ to limit memory usage.
"""
from rql.utils import VisitableMixIn
from typing import (
Tuple,
List,
Any,
Optional,
TYPE_CHECKING,
Iterator,
Union as Union_,
Dict,
Type,
TypeVar,
Iterable,
Sequence,
)
__docformat__: str = "restructuredtext en"
if TYPE_CHECKING:
import rql
_Y = TypeVar("_Y", bound="BaseNode")
[docs]class BaseNode(VisitableMixIn):
__slots__: Iterable[str] = ("parent",)
if TYPE_CHECKING:
parent: Optional["BaseNode"]
# the only class that doesn't have children is ScopeNode but it
# doesn't seems to be used explicitely anywhere and ALL its children
# have a "children" attribute, so we take the decision to make
# everything simplier and, for typing, pretend that ALL nodes have
# children
# also ScopeNode really seems to be an abstract base class
# note: ALL subclass of BaseNode (except ScopeNode) also have children
children: Sequence["BaseNode"]
# another option would be to define a new type like this:
# NodeWithChildren = Union_[Node, LeafNode, SubQuery, Set, Insert, Delete,
# Exists]
# or create a protocol in the same fashion
def __str__(self) -> str:
s: str = self.as_string()
return s
[docs] def as_string(self, kwargs: Dict = None) -> str:
"""Return the tree as an encoded rql string."""
raise NotImplementedError()
[docs] def initargs(self, stmt: Optional["rql.stmts.Statement"]) -> Tuple[Any, ...]:
"""Return list of arguments to give to __init__ to clone this node.
I don't use __getinitargs__ because I'm not sure it should interfer with
copy/pickle
"""
return ()
# pyannotate suggested rql.stmts.Union as returned type, samples=2
# but it sound like we should return a Node here
@property
def root(self) -> Optional["BaseNode"]:
"""Return the root node of the tree"""
if self.parent is not None:
return self.parent.root
return None # should we return None or something else?
@property
def stmt(self) -> Optional["rql.stmts.Statement"]:
"""Return the Select node to which this node belong"""
if self.parent is not None:
return self.parent.stmt
return None # should we return None or something else?
@property
def scope(self) -> Optional[Union_["rql.stmts.Statement", "BaseNode"]]:
"""Return the scope node to which this node belong (eg Select or Exists
node)
"""
if self.parent is not None:
return self.parent.scope
return None # should we return None or something else?
[docs] def get_nodes(self, klass: Type[_Y]) -> List[_Y]:
"""Return the list of nodes of a given class in the subtree.
:type klass: a node class (Relation, Constant, etc.)
:param klass: the class of nodes to return
:rtype: list
"""
stack: List[Any] = [self]
result = []
while stack:
node = stack.pop()
if isinstance(node, klass):
result.append(node)
else:
stack += node.children
return result
[docs] def iget_nodes(self, klass: Type[_Y]) -> Iterator[_Y]:
"""Return an iterator over nodes of a given class in the subtree.
:type klass: a node class (Relation, Constant, etc.)
:param klass: the class of nodes to return
:rtype: iterator
"""
stack: List[Any] = [self]
while stack:
node = stack.pop()
if isinstance(node, klass):
yield node
else:
stack += node.children
# Argument 1 to "is_equivalent" of "BaseNode" has incompatible type "IsOperator";
# expected "BaseNode" [arg-type]
# Argument 2 to "is_equivalent" of "BaseNode" has incompatible type "IsOperator";
# expected "BaseNode" [arg-type]
# def is_equivalent(self, other: "BaseNode") -> bool:
# Any here really means: any children of BaseNode
[docs] def is_equivalent(self: Any, other: Any) -> bool:
if not issubclass(self.__class__, other.__class__):
return False
for i, child in enumerate(self.children):
try:
if not child.is_equivalent(other.children[i]):
return False
except IndexError:
return False
return True
[docs] def copy(self, stmt: Optional["rql.stmts.Statement"] = None) -> "BaseNode":
raise NotImplementedError()
[docs] def replace(
self, old_child: "BaseNode", new_child: "BaseNode"
) -> Tuple["BaseNode", "BaseNode", Optional[int]]:
raise NotImplementedError()
[docs]class Node(BaseNode):
"""Class for nodes of the tree which may have children (almost all...)"""
__slots__: Iterable[str] = ("children",)
def __init__(self):
self.parent: Optional[BaseNode] = None
self.children: List[BaseNode] = []
[docs] def append(self, child: BaseNode):
"""add a node to children"""
self.children.append(child)
child.parent = self
[docs] def remove(
self, child: BaseNode
) -> Tuple[BaseNode, Optional[BaseNode], Optional[int]]:
"""Remove a child node. Return the removed node, its old parent and
index in the children list.
"""
index: int = self.children.index(child)
del self.children[index]
parent = child.parent
child.parent = None
return child, parent, index
[docs] def insert(self, index: int, child: BaseNode):
"""insert a child node"""
self.children.insert(index, child)
child.parent = self
[docs] def replace(
self, old_child: BaseNode, new_child: BaseNode
) -> Tuple[BaseNode, BaseNode, Optional[int]]:
"""replace a child node with another"""
i = self.children.index(old_child)
self.children.pop(i)
self.children.insert(i, new_child)
new_child.parent = self
return old_child, self, i
[docs] def copy(self, stmt: Optional["rql.stmts.Statement"] = None) -> "Node":
"""Create and return a copy of this node and its descendant.
stmt is the root node, which should be use to get new variables
"""
new: Node = self.__class__(*self.initargs(stmt))
for child in self.children:
new.append(child.copy(stmt))
return new
[docs]class BinaryNode(Node):
__slots__: Iterable[str] = ()
def __init__(
self,
lhs: Optional["BaseNode"] = None,
rhs: Optional["BaseNode"] = None,
):
Node.__init__(self)
if lhs is not None:
self.append(lhs)
if rhs is not None:
self.append(rhs)
[docs] def remove(
self, child: "BaseNode"
) -> Tuple["BaseNode", Optional["BaseNode"], Optional[int]]:
"""Remove the child and replace this node with the other child."""
index: int = self.children.index(child)
if self.parent is not None:
return self.parent.replace(self, self.children[not index])
return child, None, index
[docs] def get_parts(self) -> Tuple["BaseNode", "BaseNode"]:
"""Return the left hand side and the right hand side of this node."""
return self.children[0], self.children[1]
[docs]class LeafNode(BaseNode):
"""Class optimized for leaf nodes."""
__slots__: Iterable[str] = ()
# We can't do that because of a bug in mypy see
# https://github.com/python/mypy/issues/4125#issuecomment-337187251
# @property
# def children(self) -> Sequence["BaseNode"]:
@property
def children(self):
return ()
[docs] def copy(
self: "BaseNode", stmt: Optional["rql.stmts.Statement"] = None
) -> "BaseNode":
"""Create and return a copy of this node and its descendant.
stmt is the root node, which should be use to get new variables.
"""
return self.__class__(*self.initargs(stmt))