Module: field_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.
import datetime as dt
import types
import typing
from dataclasses import dataclass
from enum import Enum
from typing import Literal
from typing import Type
from uuid import UUID
from typing_extensions import Self
from cl.runtime.primitive.case_util import CaseUtil
from cl.runtime.records.class_info import ClassInfo
from cl.runtime.records.dataclasses_extensions import missing
from cl.runtime.schema.field_kind import FieldKind
primitive_types = (str, float, bool, int, dt.date, dt.time, dt.datetime, UUID, bytes)
"""Tuple of primitive types."""
primitive_modules = ["builtins", "datetime", "uuid"]
"""List of modules for primitive types."""
@dataclass(slots=True, kw_only=True)
class FieldDecl:
"""Field declaration."""
name: str = missing()
"""Field name."""
label: str | None = missing()
"""Field label (if not specified, titleized name is used instead)."""
comment: str | None = missing()
"""Field comment."""
field_kind: FieldKind = missing()
"""Kind of the element within the container if the field is a container, otherwise kind of the field itself."""
field_type: str = missing()
"""Field type name for builtins and uuid modules and module.ClassName for all other types."""
container_type: str | None = None
"""Container type name for builtins module and module.ClassName for other types."""
optional_field: bool = False
"""Indicates if the entire field can be None."""
optional_values: bool = False
"""Indicates if values within the container can be None if the field is a container, otherwise None."""
additive: bool = False
"""Optional flag indicating if the element is additive (i.e., its sum across records has meaning)."""
formatter: str | None = None
"""Format string used to display the element using Python conventions ."""
alternate_of: str | None = None
"""This field is an alternate of the specified field, of which only one can be specified."""
@classmethod
def create(
cls,
record_type: Type,
field_name: str,
field_type: Type,
field_comment: str,
*,
dependencies: typing.Set[Type] | None = None,
) -> Self:
"""
Create from field name and type.
Args:
record_type: Type of the record for which the field is defined
field_name: Name of the field
field_type: Type of the field obtained from get_type_hints where ForwardRefs are resolved
field_comment: Field comment (docstring), currently requires source parsing due Python limitations
dependencies: Set of types used in field or methods of the specified type, populated only if not None
"""
from cl.runtime.schema.schema import Schema # TODO: Avoid circlular dependency
result = cls()
result.name = CaseUtil.snake_to_pascal_case(field_name.removesuffix("_"))
result.comment = field_comment
# Get origin and args of the field type
field_origin = typing.get_origin(field_type)
field_args = typing.get_args(field_type)
# Note two possible forms of origin for optional, typing.Union and types.UnionType
is_union = field_origin is typing.Union or field_origin is types.UnionType
is_optional = is_union and type(None) in field_args
# Strip optional from field_type
if is_optional:
# Indicate that field can be None
result.optional_field = True
# Get type information without None
field_type = field_args[0]
field_origin = typing.get_origin(field_type)
field_args = typing.get_args(field_type)
else:
# Indicate that field cannot be None
result.optional_field = False
# Check for one of the supported container types
if field_origin in [list, dict]:
if field_origin.__module__ == "builtins":
result.container_type = field_origin.__name__
else:
result.container_type = f"{field_origin.__module__}.{field_origin.__name__}"
# Strip container information from field_type to get the type of value inside the container
field_type = field_args[0]
field_origin = typing.get_origin(field_type)
field_args = typing.get_args(field_type)
else:
# No container
result.container_type = None
# Strip optional again from the inner type
is_union = field_origin is typing.Union or field_origin is types.UnionType
is_optional = is_union and type(None) in field_args
if is_optional:
# Indicate that values can be None
result.optional_values = True
# Get type information without None
field_type = field_args[0]
field_origin = typing.get_origin(field_type)
field_args = typing.get_args(field_type)
else:
# Indicate that values cannot be None
result.optional_values = False
# Parse the value itself
if field_origin is Literal:
# List of literal strings
result.field_kind = "primitive"
result.field_type = str.__name__
elif field_origin is tuple:
# Generic key
result.field_kind = "primitive"
result.field_type = "key"
elif field_origin is None:
# Assign element kind
if field_type in primitive_types:
# Indicate that field is one of the supported primitive types
result.field_kind = "primitive"
elif issubclass(field_type, Enum):
# Indicate that field is an enum
result.field_kind = "enum"
elif field_type.__name__.endswith("Key"):
# Indicate that field is a key
result.field_kind = "key"
else:
# Indicate that field is a user-defined data or record
result.field_kind = "data"
if field_type.__module__ in primitive_modules:
# Primitive type, specify type name
result.field_type = field_type.__name__
else:
# Complex type, specify full class path
field_class_path = f"{field_type.__module__}.{field_type.__name__}"
# For keys, remove suffix
if result.field_kind == "key":
if field_class_path.endswith("Key"):
field_class_module = field_type.__module__
field_class_name = field_type.__name__
field_class_path = f"{field_class_module}.{field_class_name}"
else:
raise RuntimeError("Field has TypeKind=key but class name does not end in 'Key'.")
result.field_type = field_class_path
field_type_obj = ClassInfo.get_class_type(field_class_path)
if (
dependencies is not None
and field_type_obj is not record_type
and field_type_obj not in dependencies
):
# TODO: Do we need this if we are processing dependencies?
# TODO: Should a list of dependencies be added to TypeDecl object directly
if issubclass(field_type_obj, Enum):
from cl.runtime.schema.enum_decl import EnumDecl
# TODO: Restore call when implemented EnumDecl.for_type(field_type_obj, dependencies=dependencies)
else:
from cl.runtime.schema.type_decl import TypeDecl
TypeDecl.for_type(field_type_obj, dependencies=dependencies)
# Add to dependencies
if dependencies is not None:
dependencies.add(field_type_obj)
# Add to Schema
else:
raise RuntimeError(f"Complex type {field_type} is not recognized when building database schema.")
return result
Global variables
var primitive_modules
-
List of modules for primitive types.
var primitive_types
-
Tuple of primitive types.
Classes
class FieldDecl (*, name: str = None, label: str | None = None, comment: str | None = None, field_kind: Literal['primitive', 'enum', 'key', 'data'] = None, field_type: str = None, container_type: str | None = None, optional_field: bool = False, optional_values: bool = False, additive: bool = False, formatter: str | None = None, alternate_of: str | None = None)
-
Field declaration.
Expand source code
@dataclass(slots=True, kw_only=True) class FieldDecl: """Field declaration.""" name: str = missing() """Field name.""" label: str | None = missing() """Field label (if not specified, titleized name is used instead).""" comment: str | None = missing() """Field comment.""" field_kind: FieldKind = missing() """Kind of the element within the container if the field is a container, otherwise kind of the field itself.""" field_type: str = missing() """Field type name for builtins and uuid modules and module.ClassName for all other types.""" container_type: str | None = None """Container type name for builtins module and module.ClassName for other types.""" optional_field: bool = False """Indicates if the entire field can be None.""" optional_values: bool = False """Indicates if values within the container can be None if the field is a container, otherwise None.""" additive: bool = False """Optional flag indicating if the element is additive (i.e., its sum across records has meaning).""" formatter: str | None = None """Format string used to display the element using Python conventions .""" alternate_of: str | None = None """This field is an alternate of the specified field, of which only one can be specified.""" @classmethod def create( cls, record_type: Type, field_name: str, field_type: Type, field_comment: str, *, dependencies: typing.Set[Type] | None = None, ) -> Self: """ Create from field name and type. Args: record_type: Type of the record for which the field is defined field_name: Name of the field field_type: Type of the field obtained from get_type_hints where ForwardRefs are resolved field_comment: Field comment (docstring), currently requires source parsing due Python limitations dependencies: Set of types used in field or methods of the specified type, populated only if not None """ from cl.runtime.schema.schema import Schema # TODO: Avoid circlular dependency result = cls() result.name = CaseUtil.snake_to_pascal_case(field_name.removesuffix("_")) result.comment = field_comment # Get origin and args of the field type field_origin = typing.get_origin(field_type) field_args = typing.get_args(field_type) # Note two possible forms of origin for optional, typing.Union and types.UnionType is_union = field_origin is typing.Union or field_origin is types.UnionType is_optional = is_union and type(None) in field_args # Strip optional from field_type if is_optional: # Indicate that field can be None result.optional_field = True # Get type information without None field_type = field_args[0] field_origin = typing.get_origin(field_type) field_args = typing.get_args(field_type) else: # Indicate that field cannot be None result.optional_field = False # Check for one of the supported container types if field_origin in [list, dict]: if field_origin.__module__ == "builtins": result.container_type = field_origin.__name__ else: result.container_type = f"{field_origin.__module__}.{field_origin.__name__}" # Strip container information from field_type to get the type of value inside the container field_type = field_args[0] field_origin = typing.get_origin(field_type) field_args = typing.get_args(field_type) else: # No container result.container_type = None # Strip optional again from the inner type is_union = field_origin is typing.Union or field_origin is types.UnionType is_optional = is_union and type(None) in field_args if is_optional: # Indicate that values can be None result.optional_values = True # Get type information without None field_type = field_args[0] field_origin = typing.get_origin(field_type) field_args = typing.get_args(field_type) else: # Indicate that values cannot be None result.optional_values = False # Parse the value itself if field_origin is Literal: # List of literal strings result.field_kind = "primitive" result.field_type = str.__name__ elif field_origin is tuple: # Generic key result.field_kind = "primitive" result.field_type = "key" elif field_origin is None: # Assign element kind if field_type in primitive_types: # Indicate that field is one of the supported primitive types result.field_kind = "primitive" elif issubclass(field_type, Enum): # Indicate that field is an enum result.field_kind = "enum" elif field_type.__name__.endswith("Key"): # Indicate that field is a key result.field_kind = "key" else: # Indicate that field is a user-defined data or record result.field_kind = "data" if field_type.__module__ in primitive_modules: # Primitive type, specify type name result.field_type = field_type.__name__ else: # Complex type, specify full class path field_class_path = f"{field_type.__module__}.{field_type.__name__}" # For keys, remove suffix if result.field_kind == "key": if field_class_path.endswith("Key"): field_class_module = field_type.__module__ field_class_name = field_type.__name__ field_class_path = f"{field_class_module}.{field_class_name}" else: raise RuntimeError("Field has TypeKind=key but class name does not end in 'Key'.") result.field_type = field_class_path field_type_obj = ClassInfo.get_class_type(field_class_path) if ( dependencies is not None and field_type_obj is not record_type and field_type_obj not in dependencies ): # TODO: Do we need this if we are processing dependencies? # TODO: Should a list of dependencies be added to TypeDecl object directly if issubclass(field_type_obj, Enum): from cl.runtime.schema.enum_decl import EnumDecl # TODO: Restore call when implemented EnumDecl.for_type(field_type_obj, dependencies=dependencies) else: from cl.runtime.schema.type_decl import TypeDecl TypeDecl.for_type(field_type_obj, dependencies=dependencies) # Add to dependencies if dependencies is not None: dependencies.add(field_type_obj) # Add to Schema else: raise RuntimeError(f"Complex type {field_type} is not recognized when building database schema.") return result
Subclasses
Static methods
def create(record_type: Type, field_name: str, field_type: Type, field_comment: str, *, dependencies: Optional[Set[Type]] = None) -> Self
-
Create from field name and type.
Args
record_type
- Type of the record for which the field is defined
field_name
- Name of the field
field_type
- Type of the field obtained from get_type_hints where ForwardRefs are resolved
field_comment
- Field comment (docstring), currently requires source parsing due Python limitations
dependencies
- Set of types used in field or methods of the specified type, populated only if not None
Fields
var additive -> bool
-
Optional flag indicating if the element is additive (i.e., its sum across records has meaning).
var alternate_of -> str | None
-
This field is an alternate of the specified field, of which only one can be specified.
var comment -> str | None
-
Field comment.
var container_type -> str | None
-
Container type name for builtins module and module.ClassName for other types.
var field_kind -> Literal['primitive', 'enum', 'key', 'data']
-
Kind of the element within the container if the field is a container, otherwise kind of the field itself.
var field_type -> str
-
Field type name for builtins and uuid modules and module.ClassName for all other types.
var formatter -> str | None
-
Format string used to display the element using Python conventions .
var label -> str | None
-
Field label (if not specified, titleized name is used instead).
var name -> str
-
Field name.
var optional_field -> bool
-
Indicates if the entire field can be None.
var optional_values -> bool
-
Indicates if values within the container can be None if the field is a container, otherwise None.