"""
Autonodes are stripped-down versions of the full DaCHS structures.
The idea is to have "managed attributes light" with automatic
constructors. The Autonodes are used when building something like abstract
syntax trees in STC or ADQL.
"""
#c Copyright 2008-2023, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL. See the
#c COPYING file in the source distribution.
import itertools
from gavo.utils.dachstypes import Any, Dict, List, Tuple
[docs]class AutoNodeType(type):
"""A metaclass for AutoNodes.
The idea here is to define children in a class definition and make sure they
are actually present.
AutoNodes are supposed to be immutable; the are defined during construction.
Currently, nothing keeps you from changing them afterwards, but that may
change.
The classes' constructor is defined to accept all attributes as arguments
(you probably want to use keyword arguments here). It is the constructor
that sets up the attributes, so AutoNodes must not have an __init__ method.
However, they may define a method _setupNode that is called just before the
artificial constructor returns.
To define the attributes of the class, add _a_<attname> attributes
giving a default to the class. The default should normally be either
None for 1:1 or 1:0 relations or an empty tuple for 1:n relations.
The defaults must return a repr that constructs them, since we create
a source fragment.
"""
def __init__(cls, name: str, bases: Tuple[type, ...], dict: Dict):
cls._collectAttributes()
cls._buildConstructor()
def _collectAttributes(cls) -> None:
cls._nodeAttrs: List[Tuple[str, Any]] = []
for name in dir(cls):
if name.startswith("_a_"):
cls._nodeAttrs.append((name[3:], getattr(cls, name)))
def _buildConstructor(cls) -> None:
argList, codeLines = ["self"], []
for argName, argDefault in cls._nodeAttrs:
argList.append("%s=%s"%(argName, repr(argDefault)))
codeLines.append(" self.%s = %s"%(argName, argName))
codeLines.append(" self._setupNode()\n")
codeLines.insert(0, "def constructor(%s):"%(", ".join(argList)))
ns: Dict[str, Any] = {}
exec("\n".join(codeLines), ns)
cls.__init__ = ns["constructor"] # type: ignore
[docs]class AutoNode(object, metaclass=AutoNodeType):
"""An AutoNode.
AutoNodes are explained in AutoNode's metaclass, AutoNodeType.
A noteworthy method is change -- pass in new attribute values
to create a new instance with the original attribute values except
for those passed to change. This will only work if all non-autoattribute
attributes of the class are set in _setupNode.
"""
def _setupNodeNext(self, cls):
try:
pc = super(cls, self)._setupNode
except AttributeError:
pass
else:
pc()
def _setupNode(self):
self._setupNodeNext(AutoNode)
def __repr__(self):
return "<%s %s>"%(self.__class__.__name__, " ".join(
"%s=%s"%(name, repr(val))
for name, val in self.iterAttributes(skipEmpty=True)))
[docs] def change(self, **kwargs):
"""returns a shallow copy of self with constructor arguments in kwargs
changed.
"""
if not kwargs:
return self
consArgs = dict(self.iterAttributes())
consArgs.update(kwargs)
return self.__class__(**consArgs)
[docs] @classmethod
def cloneFrom(cls, other, **kwargs) -> 'AutoNode':
"""returns a shallow clone of other.
other should be of the same class or a superclass.
"""
consArgs = dict(other.iterAttributes())
consArgs.update(kwargs)
return cls(**consArgs)
[docs] def iterAttributes(self, skipEmpty: bool=False):
"""yields pairs of attributeName, attributeValue for this node.
"""
for name, _ in self._nodeAttrs: # type: ignore
val = getattr(self, name)
if skipEmpty and not val:
continue
yield name, val
[docs] def iterChildren(self, skipEmpty: bool=False):
for name, val in self.iterAttributes(skipEmpty):
if isinstance(val, (list, tuple)):
for c in val:
yield name, c
else:
yield name, val
[docs] def iterNodeChildren(self):
"""yields pairs of attributeName, attributeValue for this node.
This will look into sequences, so multiple occurrences of an
attributeName are possible. Only nodes are returned.
"""
for name, val in self.iterChildren(skipEmpty=True):
if isinstance(val, AutoNode) and val is not self:
yield val
[docs] def iterNodes(self):
"""iterates the tree preorder.
Only AutoNodes are returned, not python values.
"""
childIterators = [c.iterNodes() for c in self.iterNodeChildren()]
return itertools.chain((self,), *childIterators)