Source code for zensols.dataclasses.inspect
"""Very simple object relational mapping.
"""
__author__ = 'Paul Landes'
from typing import Type, Any
from dataclasses import dataclass, field, Field
import dataclasses
import logging
import re
import ast
import inspect
import textwrap
from ..config import Dictable
from ..persist import persisted
from ..introspect.imp import ClassImporter
from ..introspect.insp import ClassDoc
from ..introspect.insp import ClassField as IntrospectClassField
logger = logging.getLogger(__name__)
[docs]
@dataclass
class ClassField(IntrospectClassField, Dictable):
"""Stash column metadata that repreesnts a :class:`dataclasses.dataclass`.
"""
data_field: Field = field(repr=False)
"""The field data from the dataclass."""
@staticmethod
def _flatten_class_field(data: dict[str, Any]):
data['dtype'] = str(data['dtype'].__name__)
return data
def _flatten_dict(self, data: dict[str, Any]) -> dict[str, Any]:
data = self._flatten_class_field(data)
data = super()._flatten_dict(data)
return data
def __str__(self) -> str:
return f'{self.name} ({self.dtype.__name__})'
def __repr__(self) -> str:
return self.__str__()
[docs]
@dataclass
class DataclassMetadata(Dictable):
"""This class provides introspection access and metadata to
:class:`~dataclass.dataclass` instances.
"""
_DICTABLE_ATTRIBUTES = {'fields_by_order', 'doc'}
class_type: Type = field()
"""The dataclass to map."""
def _flatten_dict(self, data: dict[str, Any]) -> dict[str, Any]:
data['class_type'] = str(data['class_type'])
data['fields_by_order'] = tuple(map(
ClassField._flatten_class_field, data['fields_by_order']))
data['class_type'] = ClassImporter.full_classname(self.class_type)
dct = super()._flatten_dict(data)
return dct
def _get_attribute_docstrings(self) -> dict[str, str]:
cls: Type = self.class_type
source = inspect.getsource(cls)
source = textwrap.dedent(source)
tree = ast.parse(source)
class_node = next(
node for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == cls.__name__)
docs: dict[str, str] = {}
prev_field_name: str = None
for node in class_node.body:
if isinstance(node, ast.AnnAssign):
# name: str = field()
# icon: str
if isinstance(node.target, ast.Name):
prev_field_name = node.target.id
else:
prev_field_name = None
elif isinstance(node, ast.Assign):
# name = field()
if len(node.targets) == 1 and \
isinstance(node.targets[0], ast.Name):
prev_field_name = node.targets[0].id
else:
prev_field_name = None
elif (prev_field_name is not None and
isinstance(node, ast.Expr) and
isinstance(node.value, ast.Constant) and
isinstance(node.value.value, str)):
doc: str = inspect.cleandoc(node.value.value)
doc = re.sub(r'\s+', ' ', doc).strip()
docs[prev_field_name] = doc
prev_field_name = None
else:
prev_field_name = None
return docs
@property
@persisted('_fields_by_order')
def fields_by_order(self) -> tuple[ClassField, ...]:
"""Create field metadata for :obj:`class_type`."""
def map_field(f: Field) -> ClassField:
doc: str = docs.get(f.name)
return ClassField(
name=f.name,
dtype=f.type,
doc=None if doc is None else ClassDoc(doc),
kwargs={},
data_field=f)
docs: dict[str, str] = self._get_attribute_docstrings()
return tuple(map(map_field, dataclasses.fields(self.class_type)))
@property
def fields(self) -> dict[str, ClassField]:
"""The fields of the class."""
return dict(map(lambda m: (m.name, m), self.fields_by_order))
@property
@persisted('_doc')
def doc(self) -> ClassDoc:
"""The docstring of the class."""
return ClassDoc(self.class_type.__doc__)
def _write(self, c):
c(str(self.class_type), 'class_type')
c(self.doc.text, 'doc')
meta: ClassField
for meta in self.fields_by_order:
fd: dict[str, Any] = meta.asdict()
c(f"{fd.pop('name')}:")
c(fd, depth=1)