Module: type_decl
Expand source code
# Copyright (C) 2023-present The Project Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import ast
import dataclasses
import inspect
from dataclasses import asdict
from dataclasses import dataclass
from enum import Enum
from itertools import tee
from typing import Any
from typing import Dict
from typing import List
from typing import Literal
from typing import Optional
from typing import Set
from typing import Type
from typing import get_type_hints
from inflection import titleize
from memoization import cached
from typing_extensions import Self
from cl.runtime.primitive.case_util import CaseUtil
from cl.runtime.records.dataclasses_extensions import missing
from cl.runtime.records.key_util import KeyUtil
from cl.runtime.records.record_mixin import RecordMixin
from cl.runtime.schema.element_decl import ElementDecl
from cl.runtime.schema.field_decl import FieldDecl
from cl.runtime.schema.handler_declare_block_decl import HandlerDeclareBlockDecl
from cl.runtime.schema.module_decl_key import ModuleDeclKey
from cl.runtime.schema.type_decl_key import TypeDeclKey
from cl.runtime.schema.type_kind import TypeKind
DisplayKindLiteral = Literal["Basic", "Singleton", "Dashboard"] # TODO: Review
# TODO: Move this and other functions to helper class
def to_type_decl_dict(node: Dict[str, Any] | List[Dict[str, Any]] | str) -> Dict[str, Any] | List[Dict[str, Any]] | str:
"""Recursively apply type declaration dictionary conventions to the argument dictionary."""
if isinstance(node, dict):
# For type declarations only, skip nodes that have the value of None or False
# Remove suffix _ from field names if present
# pascalized_values = {k: (CaseUtil.snake_to_pascal_case(v) if k in ['module_name', 'name'] else v) for k, v in node.items()}
# Searching for the name of given type declaration
result: Dict[str, Any] = {}
if (_t := get_name_of_type_decl_dict(node)) is not None:
result["_t"] = _t
result.update(
{
(CaseUtil.snake_to_pascal_case(k.removesuffix("_")) if k != "_t" else k): to_type_decl_dict(v)
for k, v in node.items()
if v not in [None, False]
}
)
return result
elif isinstance(node, list):
# For type declarations only, skip nodes that have the value of None or False
return [to_type_decl_dict(v) for v in node if v not in [None, False]]
elif isinstance(node, tuple):
# The first element of key node tuple is type, the remaining elements are primary key fields
# Remove suffix _ from field names if present
key_field_names = node[0].get_key_fields()
key_field_values = [to_type_decl_dict(v) for v in node[1:]]
return {
CaseUtil.snake_to_pascal_case(k.removesuffix("_")): v for k, v in zip(key_field_names, key_field_values)
}
elif isinstance(node, str):
return node
else:
return node
def for_type_key_maker(
cls,
record_type: Type,
*,
dependencies: Set[Type] | None = None,
skip_fields: bool = False,
skip_handlers: bool = False,
) -> str:
"""Custom key marker for 'for_type' class method."""
# TODO: Replace by lambda if skip_fields parameter is removed
return f"{record_type.__module__}.{record_type.__name__}.{dependencies.__hash__}{skip_fields}{skip_handlers}"
def get_name_of_type_decl_dict(dict_: Dict[str, Dict]) -> Optional[str]:
"""Search for the type name in the given dict and return in format {module}.{name} ."""
# Element fields contain "key_" in case of key-field or "data" section in case of data-field
key_field = dict_.get("key_", None)
data_field = dict_.get("data", None)
name_field = "name"
module_field = "module"
module_name_field = "module_name"
module = None
name = None
if key_field is not None and name_field in key_field:
module = key_field.get(module_field, {}).get(module_name_field, None)
name = key_field[name_field]
elif data_field is not None and name_field in data_field:
module = data_field.get(module_field, {}).get(module_name_field, None)
name = data_field[name_field]
# Name of the whole type is contained in "name" field. But type decl cannot contain only "name" and "module" fields
elif (name_ := dict_.get(name_field, None)) is not None and module_field in dict_:
if len(dict_) > 2:
name = name_
type_name = f"{module}.{name}" if module is not None else name
return type_name
@dataclass(slots=True, kw_only=True)
class TypeDecl(TypeDeclKey, RecordMixin[TypeDeclKey]):
"""Provides information about a class, its fields, and its methods."""
label: str | None = missing()
"""Type label."""
comment: str | None = missing()
"""Type comment. Contains additional information."""
kind: TypeKind | None = missing()
"""Type kind."""
display_kind: DisplayKindLiteral = missing() # TODO: Make optional, treat None as Basic
"""Display kind."""
inherit: TypeDeclKey | None = missing()
"""Parent type reference."""
declare: HandlerDeclareBlockDecl | None = missing() # TODO: Flatten or use block for abstract flag
"""Handler declaration block."""
elements: List[ElementDecl] | None = missing() # TODO: Consider renaming to fields
"""Element declaration block."""
keys: List[str] | None = missing()
"""Array of key element names (specify in base class only)."""
# TODO: Consider moving to Table class
# indexes: List[TypeIndexDecl] | None = missing()
"""Defines indexes for the type."""
immutable: bool | None = missing()
"""Immutable flag."""
permanent: bool | None = missing()
"""When the record is saved, also save it permanently."""
def get_key(self) -> TypeDeclKey:
return TypeDeclKey(module=self.module, name=self.name)
def to_type_decl_dict(self) -> Dict[str, Any]:
"""Convert to dictionary using type declaration conventions."""
# Convert to standard dictionary format
standard_dict = asdict(self)
# Apply type declaration dictionary conventions
result = to_type_decl_dict(standard_dict)
return result
@classmethod
def for_key(cls, key: TypeDeclKey) -> Self:
"""Create or return cached object for the specified type declaration key."""
class_path = f"{key.module.module_name}.{key.name}"
return cls.for_class_path(class_path)
@classmethod
def for_class_path(cls, class_path: str) -> Self:
"""Create or return cached object for the specified class path in module.ClassName format."""
raise NotImplementedError()
@classmethod
@cached(custom_key_maker=for_type_key_maker)
def for_type(
cls,
record_type: Type,
*,
dependencies: Set[Type] | None = None,
skip_fields: bool = False,
skip_handlers: bool = False,
) -> Self:
"""
Create or return cached object for the specified record type.
Args:
record_type: Type of the record for which the declaration is created
dependencies: Set of types used in field or methods of the specified type, populated only if not None
skip_fields: Use this flag to skip fields generation when the method is invoked from a derived class
skip_handlers: Use this flag to skip handlers generation when the method is invoked internal methods
"""
if issubclass(record_type, Enum):
raise RuntimeError(f"Cannot create TypeDecl for class {record_type.__name__} because it is an enum.")
if issubclass(record_type, tuple):
raise RuntimeError(f"Cannot create TypeDecl for class {record_type.__name__} because it is a tuple.")
# Create instance of the final type
result = cls()
result.module = ModuleDeclKey(module_name=record_type.__module__)
result.name = record_type.__name__
result.label = titleize(result.name) # TODO: Add override from settings
result.comment = record_type.__doc__
# Set type kind by detecting the presence of 'get_key' method to indicate a record vs. an element
is_record = hasattr(record_type, "get_key")
is_abstract = hasattr(record_type, "__abstractmethods__") and bool(record_type.__abstractmethods__)
if is_record:
result.kind = "abstract" if is_abstract else None
else:
result.kind = "abstract_element" if is_abstract else "Element"
# Set display kind
result.display_kind = "Basic" # TODO: Remove Basic after display_kind is made optional
# Set parent class as the first class in MRO that is not self and does not have Mixin suffix
for parent_type in record_type.__mro__:
# TODO: Refactor to make it work not only for dataclasses
if (
parent_type is not record_type
and not parent_type.__name__.endswith("Mixin")
and not parent_type.__name__.endswith("Key")
and dataclasses.is_dataclass(parent_type)
):
parent_type_decl = cls.for_type(parent_type, dependencies=dependencies)
result.inherit = parent_type_decl.get_key()
# Add to dependencies
if dependencies is not None:
dependencies.add(parent_type)
# Get type public methods
if not skip_handlers:
handlers_block = HandlerDeclareBlockDecl.get_type_methods(record_type, inherit=True)
if handlers_block.handlers:
result.declare = handlers_block
# Get key fields by parsing the source of 'get_key' method and convert to PascalCase
snake_case_key_fields = KeyUtil.get_key_fields(record_type)
if snake_case_key_fields is not None:
pascal_case_key_fields = [CaseUtil.snake_to_pascal_case(x) for x in snake_case_key_fields]
result.keys = pascal_case_key_fields # TODO: Use slots of key type when present?
# Use this flag to skip fields generation when the method is invoked from a derived class
if not skip_fields:
# Get type hints to resolve ForwardRefs
type_hints = get_type_hints(record_type)
# Dictionary of member comments (docstrings), currently requires source parsing due Python limitations
member_comments = cls.get_member_comments(record_type)
# Add an element for each type hint
result.elements = []
for field_name, field_type in type_hints.items():
# Skip protected fields
if field_name.startswith("_"):
continue
# Field comment (docstring)
field_comment = member_comments.get(field_name, None)
# Get the rest of the data from the field itself
field_decl = FieldDecl.create(
record_type, field_name, field_type, field_comment, dependencies=dependencies
)
# Convert to element and add
element_decl = ElementDecl.create(field_decl)
result.elements.append(element_decl)
return result
@classmethod
@cached
def get_member_comments(cls, record_type: type) -> Dict[str, str]:
"""Extract class member comments."""
# Include comments from key class fields for base
# TODO: Revise approach to key fields
if len(record_type.__mro__) > 1 and record_type.__mro__[1].__name__.endswith("Key"):
comments = cls.get_member_comments(record_type.__mro__[1])
else:
comments = dict()
ast_tree = ast.parse(inspect.getsource(record_type))
for i, j in cls.by_pair(ast.iter_child_nodes(ast_tree.body[0])):
if isinstance(i, ast.AnnAssign):
target_node = i.target
elif isinstance(i, ast.Assign):
target_node = i.targets[0]
else:
continue
if not isinstance(target_node, ast.Name):
continue
name: str = target_node.id
# TODO: ast.Str is replaced by ast.Constant in Python 3.8, update
if isinstance(j, ast.Expr) and isinstance(j.value, ast.Str):
comments[name] = inspect.cleandoc(j.value.s)
return comments
@classmethod
def by_pair(cls, iterable):
"""s -> (s0,s1), (s1,s2), (s2, s3), ..."""
a, b = tee(iterable)
next(b, None)
return zip(a, b)
Functions
def for_type_key_maker(cls, record_type: Type, *, dependencies: Set[Type] | None = None, skip_fields: bool = False, skip_handlers: bool = False) -> str
-
Custom key marker for ‘for_type’ class method.
def get_name_of_type_decl_dict(dict_: Dict[str, Dict]) -> str | None
-
Search for the type name in the given dict and return in format {module}.{name} .
def to_type_decl_dict(node: Dict[str, Any] | List[Dict[str, Any]] | str) -> Union[Dict[str, Any], List[Dict[str, Any]], str]
-
Recursively apply type declaration dictionary conventions to the argument dictionary.
Classes
class TypeDecl (*, module: ModuleDeclKey = None, name: str = None, label: str | None = None, comment: str | None = None, kind: TypeKind | None = None, display_kind: DisplayKindLiteral = None, inherit: TypeDeclKey | None = None, declare: HandlerDeclareBlockDecl | None = None, elements: List[ElementDecl] | None = None, keys: List[str] | None = None, immutable: bool | None = None, permanent: bool | None = None)
-
Provides information about a class, its fields, and its methods.
Expand source code
@dataclass(slots=True, kw_only=True) class TypeDecl(TypeDeclKey, RecordMixin[TypeDeclKey]): """Provides information about a class, its fields, and its methods.""" label: str | None = missing() """Type label.""" comment: str | None = missing() """Type comment. Contains additional information.""" kind: TypeKind | None = missing() """Type kind.""" display_kind: DisplayKindLiteral = missing() # TODO: Make optional, treat None as Basic """Display kind.""" inherit: TypeDeclKey | None = missing() """Parent type reference.""" declare: HandlerDeclareBlockDecl | None = missing() # TODO: Flatten or use block for abstract flag """Handler declaration block.""" elements: List[ElementDecl] | None = missing() # TODO: Consider renaming to fields """Element declaration block.""" keys: List[str] | None = missing() """Array of key element names (specify in base class only).""" # TODO: Consider moving to Table class # indexes: List[TypeIndexDecl] | None = missing() """Defines indexes for the type.""" immutable: bool | None = missing() """Immutable flag.""" permanent: bool | None = missing() """When the record is saved, also save it permanently.""" def get_key(self) -> TypeDeclKey: return TypeDeclKey(module=self.module, name=self.name) def to_type_decl_dict(self) -> Dict[str, Any]: """Convert to dictionary using type declaration conventions.""" # Convert to standard dictionary format standard_dict = asdict(self) # Apply type declaration dictionary conventions result = to_type_decl_dict(standard_dict) return result @classmethod def for_key(cls, key: TypeDeclKey) -> Self: """Create or return cached object for the specified type declaration key.""" class_path = f"{key.module.module_name}.{key.name}" return cls.for_class_path(class_path) @classmethod def for_class_path(cls, class_path: str) -> Self: """Create or return cached object for the specified class path in module.ClassName format.""" raise NotImplementedError() @classmethod @cached(custom_key_maker=for_type_key_maker) def for_type( cls, record_type: Type, *, dependencies: Set[Type] | None = None, skip_fields: bool = False, skip_handlers: bool = False, ) -> Self: """ Create or return cached object for the specified record type. Args: record_type: Type of the record for which the declaration is created dependencies: Set of types used in field or methods of the specified type, populated only if not None skip_fields: Use this flag to skip fields generation when the method is invoked from a derived class skip_handlers: Use this flag to skip handlers generation when the method is invoked internal methods """ if issubclass(record_type, Enum): raise RuntimeError(f"Cannot create TypeDecl for class {record_type.__name__} because it is an enum.") if issubclass(record_type, tuple): raise RuntimeError(f"Cannot create TypeDecl for class {record_type.__name__} because it is a tuple.") # Create instance of the final type result = cls() result.module = ModuleDeclKey(module_name=record_type.__module__) result.name = record_type.__name__ result.label = titleize(result.name) # TODO: Add override from settings result.comment = record_type.__doc__ # Set type kind by detecting the presence of 'get_key' method to indicate a record vs. an element is_record = hasattr(record_type, "get_key") is_abstract = hasattr(record_type, "__abstractmethods__") and bool(record_type.__abstractmethods__) if is_record: result.kind = "abstract" if is_abstract else None else: result.kind = "abstract_element" if is_abstract else "Element" # Set display kind result.display_kind = "Basic" # TODO: Remove Basic after display_kind is made optional # Set parent class as the first class in MRO that is not self and does not have Mixin suffix for parent_type in record_type.__mro__: # TODO: Refactor to make it work not only for dataclasses if ( parent_type is not record_type and not parent_type.__name__.endswith("Mixin") and not parent_type.__name__.endswith("Key") and dataclasses.is_dataclass(parent_type) ): parent_type_decl = cls.for_type(parent_type, dependencies=dependencies) result.inherit = parent_type_decl.get_key() # Add to dependencies if dependencies is not None: dependencies.add(parent_type) # Get type public methods if not skip_handlers: handlers_block = HandlerDeclareBlockDecl.get_type_methods(record_type, inherit=True) if handlers_block.handlers: result.declare = handlers_block # Get key fields by parsing the source of 'get_key' method and convert to PascalCase snake_case_key_fields = KeyUtil.get_key_fields(record_type) if snake_case_key_fields is not None: pascal_case_key_fields = [CaseUtil.snake_to_pascal_case(x) for x in snake_case_key_fields] result.keys = pascal_case_key_fields # TODO: Use slots of key type when present? # Use this flag to skip fields generation when the method is invoked from a derived class if not skip_fields: # Get type hints to resolve ForwardRefs type_hints = get_type_hints(record_type) # Dictionary of member comments (docstrings), currently requires source parsing due Python limitations member_comments = cls.get_member_comments(record_type) # Add an element for each type hint result.elements = [] for field_name, field_type in type_hints.items(): # Skip protected fields if field_name.startswith("_"): continue # Field comment (docstring) field_comment = member_comments.get(field_name, None) # Get the rest of the data from the field itself field_decl = FieldDecl.create( record_type, field_name, field_type, field_comment, dependencies=dependencies ) # Convert to element and add element_decl = ElementDecl.create(field_decl) result.elements.append(element_decl) return result @classmethod @cached def get_member_comments(cls, record_type: type) -> Dict[str, str]: """Extract class member comments.""" # Include comments from key class fields for base # TODO: Revise approach to key fields if len(record_type.__mro__) > 1 and record_type.__mro__[1].__name__.endswith("Key"): comments = cls.get_member_comments(record_type.__mro__[1]) else: comments = dict() ast_tree = ast.parse(inspect.getsource(record_type)) for i, j in cls.by_pair(ast.iter_child_nodes(ast_tree.body[0])): if isinstance(i, ast.AnnAssign): target_node = i.target elif isinstance(i, ast.Assign): target_node = i.targets[0] else: continue if not isinstance(target_node, ast.Name): continue name: str = target_node.id # TODO: ast.Str is replaced by ast.Constant in Python 3.8, update if isinstance(j, ast.Expr) and isinstance(j.value, ast.Str): comments[name] = inspect.cleandoc(j.value.s) return comments @classmethod def by_pair(cls, iterable): """s -> (s0,s1), (s1,s2), (s2, s3), ...""" a, b = tee(iterable) next(b, None) return zip(a, b)
Ancestors
- TypeDeclKey
- KeyMixin
- abc.ABC
- RecordMixin
- typing.Generic
Subclasses
Static methods
def by_pair(iterable)
-
s -> (s0,s1), (s1,s2), (s2, s3), …
def for_class_path(class_path: str) -> Self
-
Create or return cached object for the specified class path in module.ClassName format.
def for_key(key: TypeDeclKey) -> Self
-
Create or return cached object for the specified type declaration key.
def for_type(cls, record_type: Type, *, dependencies: Set[Type] | None = None, skip_fields: bool = False, skip_handlers: bool = False) -> Self
-
Create or return cached object for the specified record type.
Args
record_type
- Type of the record for which the declaration is created
dependencies
- Set of types used in field or methods of the specified type, populated only if not None
skip_fields
- Use this flag to skip fields generation when the method is invoked from a derived class
skip_handlers
- Use this flag to skip handlers generation when the method is invoked internal methods
def get_key_type() -> Type
-
Inherited from:
TypeDeclKey
.get_key_type
Return key type even when called from a record.
def get_member_comments(cls, record_type: type) -> Dict[str, str]
-
Extract class member comments.
Fields
var comment -> str | None
-
Type comment. Contains additional information.
var declare -> HandlerDeclareBlockDecl | None
-
Handler declaration block.
var display_kind -> Literal['Basic', 'Singleton', 'Dashboard']
-
Display kind.
var elements -> Optional[List[ElementDecl]]
-
Element declaration block.
var immutable -> bool | None
-
Immutable flag.
var inherit -> TypeDeclKey | None
-
Parent type reference.
var keys -> Optional[List[str]]
-
Array of key element names (specify in base class only).
var kind -> Optional[Literal['final', 'abstract', 'element', 'abstract_element']]
-
Type kind.
var label -> str | None
-
Type label.
var module -> ModuleDeclKey
-
Inherited from:
TypeDeclKey
.module
Module reference.
var name -> str
-
Inherited from:
TypeDeclKey
.name
Type name is unique when combined with module.
var permanent -> bool | None
-
When the record is saved, also save it permanently.
Methods
def get_key(self) -> TypeDeclKey
-
Inherited from:
RecordMixin
.get_key
Return a new key object whose fields populated from self, do not return self.
def init_all(self) -> None
-
Inherited from:
RecordMixin
.init_all
Invoke ‘init’ for each class in the order from base to derived, then validate against schema.
def to_type_decl_dict(self) -> Dict[str, Any]
-
Convert to dictionary using type declaration conventions.