src/sections/meta.py
from copy import copy
from types import FunctionType
from typing import Any
from typing import List
from typing import Tuple
from typing import Union
from .types import AnyDict
from .types import SectionAttr
from .types import SectionAttrs
from .types import SectionKeys
from .types import SectionKeysOrObjects
from .types import SectionNone
from .types import SectionParent
from .types import SectionType
class MetaSection(type):
"""
Parses args and kwds passed to a sections() call or :class:`Section
<Section>` instantiation and returns a Section tree structure. Parses
node names/keys, separate attrs intended for current node vs child nodes,
constructs current node, then recursively repeats for all child nodes.
"""
singular_keyname = 'name'
plural_keyname = 'names'
default_keyvalue = SectionNone
list_attr_prefix = '_'
##########################################################################
# Tree structure node construction #
def __call__(
self,
*args: SectionKeysOrObjects,
parent: SectionParent = None,
**kwds: SectionAttr
) -> SectionType:
"""
Construct a tree structure of Section nodes based on the args and kwds
provided by user in a sections() call or a Section() instantiation.
"""
args = list(args)
node_attrs, children_attrs, keyname = self.__parse_attrs(
args, kwds, parent)
node = self.__construct_node(parent, node_attrs)
self.__construct_children(node, args, children_attrs, keyname)
return node
def __parse_attrs(
self,
args: SectionKeysOrObjects,
kwds: SectionAttr,
parent: SectionParent
) -> Tuple[SectionAttrs, SectionAttrs, str]:
"""
From user-provided args and kwds in a sections() or Section() call,
parse node names/keys, separate attrs intended for current node vs
child nodes, construct current node, then recursively repeat for all
child nodes.
"""
node_attrs, children_attrs = {}, {}
keyname = self.singular_keyname
keys = self.__parse_keys(args, kwds, keyname)
for k, v in {**kwds, **keys}.items():
self.__parse_node_attrs(k, v, node_attrs, children_attrs)
node_attrs.pop(self.plural_keyname, None)
children_attrs.pop(self.plural_keyname, None)
self.__fix_node_key_if_invalid(node_attrs, parent, keyname)
if children_attrs.get(keyname):
_fix_children_keys_if_invalid(children_attrs, keyname)
node_attrs['_Section__keyname'] = keyname
return node_attrs, children_attrs, keyname
def __parse_node_attrs(
self, name: str, value: Any, node_attrs: SectionAttrs,
children_attrs: SectionAttrs
) -> None:
"""
Extract attrs intended for current node from user-provided args/kwds.
"""
if (not isinstance(value, list)
or name.startswith(self.list_attr_prefix)):
node_attrs[name] = value
else:
if len(value) > 0 and isinstance(value[0], set):
value = copy(value)
node_attrs[name] = value.pop(0).copy().pop()
children_attrs[name] = value
def __parse_keys(
self,
args: SectionKeysOrObjects,
kwds: SectionAttr,
keyname: str
) -> None:
keys = {}
if _dict_haskey(kwds, keyname):
keys[keyname] = kwds.get(keyname)
if (not _dict_haskey(kwds, keyname)
and _dict_haskey(kwds, self.plural_keyname)):
keys[keyname] = kwds.get(self.plural_keyname)
if not _dict_haskey(keys, keyname):
keys[keyname] = self.__getkeys_from_argskwds(args, kwds)
return keys
def __getkeys_from_argskwds(
self,
args: SectionKeysOrObjects,
kwds: SectionAttrs
) -> SectionKeys:
"""
Parse keys from args or kwds if it wasn't explicitly provided in kwds.
"""
if _args_is_str_and_sections(*args):
args[0] = {args[0]}
from sections import Section
if (len(args) == 1 and not isinstance(args[0], list)
and not isinstance(args[0], Section)):
# if given a single non-list argument it will be used only as
# current node's name/key
return list(args)[0]
elif len(args) >= 1:
# otherwise if any arguments were given, they are for at least
# one child's' names/keys, and possibly the current node's also
return list(args)
else:
# otherwise, use the default for the current node
return self.default_keyvalue
def __fix_node_key_if_invalid(
self, attrs: SectionAttrs, parent: SectionParent, keyname: str
) -> None:
"""
Enforce that node must have a keyname attr, and disallow key values
from properties and FunctionTypes.
"""
default_keyvalue = (self.default_keyvalue if parent is None
else parent.nofchildren)
keyvalue = attrs.get(keyname, default_keyvalue)
if (isinstance(keyvalue, FunctionType)
or isinstance(keyvalue, property)):
keyvalue = default_keyvalue
attrs[keyname] = keyvalue
def __construct_node(
self, parent: SectionParent, attrs: SectionAttrs
) -> SectionType:
"""
Construct current node by providing node all its attrs, then update
tree structure's class with any provided propertied or methods.
"""
class_attrs, node_attrs = {}, {}
for k, v in attrs.items():
if isinstance(v, FunctionType) or isinstance(v, property):
class_attrs[k] = v
else:
node_attrs[k] = v
node = super().__call__(parent=parent, **node_attrs)
for k, v in class_attrs.items():
setattr(node.__class__, k, v)
return node
def __construct_children(
self,
node: SectionType,
args: SectionKeysOrObjects,
children_attrs: SectionAttrs,
keyname: str
) -> None:
"""
Recursively repeat construction per child with extracted child attrs.
"""
nofchildren_from_attrs, children_from_args = (
_get_children_data(args, children_attrs)
)
for i, child in enumerate(children_from_args):
key = getattr(child, keyname)
if key is SectionNone:
key = i
node[key] = child
for child_i in range(nofchildren_from_attrs):
self.__contruct_child(child_i, children_attrs, node, keyname)
def __contruct_child(
self,
child_i: int, children_attrs: SectionAttrs,
node: SectionType, keyname: str
) -> None:
"""Parse attr[i] from each attr and give to child."""
child_attrs = {}
for k, v in children_attrs.items():
if len(v) > child_i:
child_attrs[k] = v[child_i]
child = _get_dictval_i(node, child_i)
self.__contruct_child_from_dict_or_cls(
child, child_attrs, child_i, keyname, node)
def __contruct_child_from_dict_or_cls(
self,
child: Union[SectionType, None],
child_attrs: SectionAttrs,
child_i: int,
keyname: str,
node: SectionType,
) -> None:
if child: # if child is Section instance
for name, value in child_attrs.items():
setattr(child, name, value)
else:
child_attrs[keyname] = child_attrs.get(keyname, child_i)
child = self(parent=node, **child_attrs)
node[getattr(child, keyname)] = child
def _fix_children_keys_if_invalid(child_attrs, keyname):
from sections import Section
keys = child_attrs[keyname]
newkeys = []
for key in keys:
if not isinstance(key, Section):
newkeys.append(key)
child_attrs[keyname] = newkeys
def _get_children_data(
args: SectionKeysOrObjects,
attrs: SectionAttrs
) -> Tuple[int, List[SectionType]]:
"""
Return number of children nodes implied by provided self.__call__ kwds,
and any pre-constructed Section children passed in self.__call__ args.
"""
nofchildren_from_attrs = (
max(_len(v) for v in attrs.values()) if attrs else 0)
children_from_args = []
for arg in args:
from . import Section
if isinstance(arg, Section):
children_from_args += [arg]
return (nofchildren_from_attrs, children_from_args)
def _len(x: Any) -> int:
"""Return len of x if it is iterable, else 0."""
return max(1, len(x)) if isinstance(x, list) else 0
def _dict_haskey(d: AnyDict, key: Any) -> bool:
"""
Return True if dict contains user-provided value for key, else False.
"""
return d.get(key, SectionNone) is not SectionNone
def _get_dictval_i(d: AnyDict, i: int) -> Any:
"""Get value in iterator position i from dict as if its a list."""
ret = None
for ii, value in enumerate(d.values()):
if ii == i:
ret = value
break
return ret
def _args_is_str_and_sections(*args: Any):
if len(args) <= 1:
args_is_str_and_sections = False
else:
args_is_str_and_sections = isinstance(args[0], str)
from sections import Section
for arg in args[1:]:
if not isinstance(arg, Section):
args_is_str_and_sections = False
break
return args_is_str_and_sections