204 lines
5.3 KiB
Python
204 lines
5.3 KiB
Python
from __future__ import annotations
|
|
|
|
import weakref
|
|
from collections.abc import Mapping
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from .ast import AST
|
|
from .infos import CommentInfo, ParseInfo
|
|
from .util import AsJSONMixin, asjson, asjsons
|
|
|
|
BASE_CLASS_TOKEN = '::' # noqa: S105
|
|
|
|
|
|
@dataclass(eq=False)
|
|
class Node(AsJSONMixin):
|
|
_parent: Node | None = None
|
|
_children: list[Node] | None = None
|
|
ast: AST | None = None
|
|
ctx: Any = None
|
|
parseinfo: ParseInfo | None = None
|
|
|
|
def __init__(self, ast=None, **attributes):
|
|
super().__init__()
|
|
if isinstance(ast, dict):
|
|
ast = AST(ast)
|
|
self.ast = ast
|
|
|
|
for name, value in attributes.items():
|
|
setattr(self, name, value)
|
|
|
|
self.__post_init__()
|
|
|
|
def __post_init__(self):
|
|
ast = self.ast
|
|
|
|
if not self.parseinfo and isinstance(ast, AST):
|
|
self.parseinfo = ast.parseinfo
|
|
|
|
if not isinstance(ast, Mapping):
|
|
return
|
|
|
|
for name in set(ast) - {'parseinfo'}:
|
|
try:
|
|
setattr(self, name, ast[name])
|
|
except AttributeError as e:
|
|
raise AttributeError(f"'{name}' is a reserved name") from e
|
|
|
|
self._children = self.children_list()
|
|
if self.parseinfo is None:
|
|
del self.parseinfo
|
|
|
|
@property
|
|
def parent(self):
|
|
return self._parent
|
|
|
|
@property
|
|
def line(self):
|
|
if self.parseinfo:
|
|
return self.parseinfo.line
|
|
return None
|
|
|
|
@property
|
|
def endline(self):
|
|
if self.parseinfo:
|
|
return self.parseinfo.endline
|
|
return None
|
|
|
|
def text_lines(self):
|
|
if self.parseinfo:
|
|
return self.parseinfo.text_lines()
|
|
return None
|
|
|
|
def line_index(self):
|
|
return self.parseinfo.line_index()
|
|
|
|
@property
|
|
def col(self):
|
|
return self.line_info.col if self.line_info else None
|
|
|
|
@property
|
|
def context(self):
|
|
return self.ctx
|
|
|
|
@property
|
|
def line_info(self):
|
|
if self.parseinfo:
|
|
return self.parseinfo.tokenizer.line_info(self.parseinfo.pos)
|
|
return None
|
|
|
|
@property
|
|
def text(self):
|
|
if not self.parseinfo:
|
|
return ''
|
|
text = self.parseinfo.tokenizer.text
|
|
return text[self.parseinfo.pos: self.parseinfo.endpos]
|
|
|
|
@property
|
|
def comments(self):
|
|
if self.parseinfo:
|
|
return self.parseinfo.tokenizer.comments(self.parseinfo.pos)
|
|
return CommentInfo([], [])
|
|
|
|
@property
|
|
def _deref(self):
|
|
# use this to get the actual object over weakref instances
|
|
return self
|
|
|
|
def _find_children(self):
|
|
def with_parent(node):
|
|
node._parent = weakref.proxy(self)
|
|
return node
|
|
|
|
def children_of(child):
|
|
if isinstance(child, weakref.ReferenceType | weakref.ProxyType):
|
|
return
|
|
elif isinstance(child, Node):
|
|
yield with_parent(child)
|
|
elif isinstance(child, Mapping):
|
|
for name, value in child.items():
|
|
if name.startswith('_'):
|
|
continue
|
|
yield from children_of(value)
|
|
elif isinstance(child, list | tuple):
|
|
yield from (
|
|
with_parent(c) for c in child if isinstance(c, Node)
|
|
)
|
|
|
|
children = list(self._pubdict().items())
|
|
if not children:
|
|
yield from children_of(self.ast)
|
|
else:
|
|
for name, child in children:
|
|
if name.startswith('_'):
|
|
continue
|
|
yield from children_of(child)
|
|
|
|
def children_list(self):
|
|
if self._children is not None:
|
|
return self._children
|
|
return list(self._find_children())
|
|
|
|
def children_set(self):
|
|
return set(self.children_list())
|
|
|
|
def children(self):
|
|
return self.children_list()
|
|
|
|
def asjson(self):
|
|
return asjson(self)
|
|
|
|
def _pubdict(self):
|
|
return {
|
|
name: value
|
|
for name, value in super()._pubdict().items()
|
|
if name not in {'ast', 'ctx', 'parent', 'parseinfo'}
|
|
}
|
|
|
|
def __str__(self):
|
|
return asjsons(self)
|
|
|
|
def __hash__(self):
|
|
if self.ast is not None:
|
|
if isinstance(self.ast, list):
|
|
return hash(tuple(self.ast))
|
|
elif isinstance(self.ast, dict):
|
|
return hash(AST(self.ast))
|
|
else:
|
|
return hash(self.ast)
|
|
else:
|
|
return id(self)
|
|
|
|
def __eq__(self, other):
|
|
if id(self) == id(other):
|
|
return True
|
|
elif self.ast is None:
|
|
return False
|
|
elif not getattr(other, 'ast', None):
|
|
return False
|
|
else:
|
|
return self.ast == other.ast
|
|
|
|
def _nonrefdict(self):
|
|
return {
|
|
name: value
|
|
for name, value in vars(self).items()
|
|
if (
|
|
name not in {'_parent', '_children'}
|
|
and type(value)
|
|
not in {weakref.ReferenceType, weakref.ProxyType}
|
|
)
|
|
}
|
|
|
|
# NOTE: pickling is important for parallel parsing
|
|
def __getstate__(self):
|
|
return self._nonrefdict()
|
|
|
|
def __setstate__(self, state):
|
|
self.__dict__.update(state)
|
|
self.children_list()
|
|
|
|
|
|
ParseModel = Node
|