Skip to content

lythonic.types

Type mapping and conversion utilities.

Types: Type mapping system for converting between Python, JSON, and SQLite types.

This module provides a registry of known types and their conversion functions for seamless data transformation between different representations.

Core Concepts

  • KnownType: A registered type with bidirectional mappings (string, JSON, DB)
  • KnownTypesMap: Registry of all known types, accessed via KNOWN_TYPES
  • JsonBase: Pydantic BaseModel with type discrimination via type_gref

Type Mappings

Each KnownType provides three MapPairs for converting to/from: - string: For CLI arguments, environment variables, config files - json: For JSON serialization (API responses, storage) - db: For SQLite storage (maps to INTEGER, REAL, TEXT, or BLOB)

Built-in Types

Primitives: int, float, str, bool, bytes Dates: date, datetime (ISO format strings) Enums: Enum (string values), IntEnum (integer values) Pydantic: BaseModel, JsonBase (JSON dicts) Paths: Path (string representation)

Usage

from lythonic.types import KNOWN_TYPES, KnownType

# Resolve a type
kt = KNOWN_TYPES.resolve_type(datetime)

# Convert from string
dt = kt.string.map_from("2024-01-15T10:30:00")

# Convert to JSON
json_val = kt.json.map_to(dt)  # "2024-01-15T10:30:00"

# Convert to DB
db_val = kt.db.map_to(dt)  # "2024-01-15T10:30:00"

Registering Custom Types

from lythonic.types import KNOWN_TYPES, KnownTypeArgs

KNOWN_TYPES.register(
    KnownTypeArgs(
        concrete_type=MyType,
        map_from_string=MyType.parse,
        map_to_string=str,
    )
)

JsonBase

JsonBase extends Pydantic's BaseModel with automatic type discrimination:

from lythonic.types import JsonBase

class Dog(JsonBase):
    name: str

class Cat(JsonBase):
    name: str

# Serialize includes type reference
dog = Dog(name="Rex")
data = dog.to_json()  # {"type_gref": "mymodule:Dog", "name": "Rex"}

# Deserialize resolves correct type
animal = JsonBase.from_json(data)  # Returns Dog instance

KnownType

Source code in src/lythonic/types.py
class KnownType:
    name: str
    aliases: set[str]
    type_: type
    is_abstract: bool
    string: MapPair[str]
    json: MapPair[Any]
    db: MapPair[Any]

    @classmethod
    def ensure(cls, type_: type | str | KnownType) -> KnownType:
        if isinstance(type_, KnownType):
            return type_
        return KNOWN_TYPES.resolve_type(type_)

    def __init__(
        self,
        args: KnownTypeArgs,
    ) -> None:
        assert not args.is_factory, "factory types cannot be initialized directly"
        self.type_ = args.get_type()
        self.is_abstract = args.is_abstract()
        args.resolve_the_rest()
        assert (
            args.db_type is not None
            and args.map_from_db is not None
            and args.map_to_db is not None
            and args.json_type is not None
            and args.map_from_json is not None
            and args.map_to_json is not None
            and args.map_from_string is not None
            and args.map_to_string is not None
        ), f"{self.type_} has not been resolved"

        self.string = MapPair(args.map_from_string, args.map_to_string, str)  # pyright: ignore
        self.json = MapPair(args.map_from_json, args.map_to_json, args.json_type)
        self.db = MapPair(args.map_from_db, args.map_to_db, args.db_type)
        self.name = (args.name if args.name is not None else self.type_.__name__).lower()
        self.aliases = set()
        self.aliases.add(self.name)
        if args.aliases is not None:
            for alias in args.aliases:
                self.aliases.add(alias.lower())

    def get_type(self) -> type:
        return self.type_

    @property
    def db_type_info(self) -> DbTypeInfo:
        return DbTypeInfo.from_type(self.db.target_type)

    @override
    def __repr__(self) -> str:
        return f"KnownType({self.name!r}, {self.type_!r})"

KnownTypeArgs

Bases: BaseModel

Source code in src/lythonic/types.py
class KnownTypeArgs(BaseModel):
    map_from_string: Callable[[str], Any] | None = None
    map_to_string: Callable[[Any], str] | None = None
    map_from_json: Callable[[Any], Any] | None = None
    map_to_json: Callable[[Any], Any] | None = None
    map_from_db: Callable[[Any], Any] | None = None
    map_to_db: Callable[[Any], Any] | None = None
    db_type: type | None = None
    json_type: type | None = None
    concrete_type: type | None = None
    abstract_type: type | None = None
    is_factory: bool = False
    name: str | None = None
    aliases: list[str] | None = None

    def get_type(self) -> type:
        assert self.abstract_type is not None or self.concrete_type is not None
        if self.concrete_type is not None:
            assert self.abstract_type is None and not self.is_factory, (
                "concrete types cannot be factories"
            )
            return self.concrete_type
        assert self.abstract_type is not None, " Redundant but no other way to shut up pyright"
        return self.abstract_type

    def is_abstract(self) -> bool:
        return self.abstract_type is self.get_type()

    def build_concrete_type(self, concrete_type: type) -> Self:
        assert self.is_factory, "only factory types can build concrete types"
        super_type = self.get_type()
        assert issubclass(concrete_type, super_type), (
            f"concrete type {concrete_type} is not a subclass of abstract type {super_type}"
        )
        clone = self.model_copy()
        clone.concrete_type = concrete_type
        clone.abstract_type = None
        clone.is_factory = False

        def remap_constructor_from_super_type_to_concrete_type(
            x: Callable[[Any], Any] | None,
        ) -> Callable[[Any], Any] | None:
            if x is None:
                return None
            if x is super_type:  # switch to constructor of concrete type
                return concrete_type
            if (
                inspect.ismethod(x) and x.__self__ is super_type
            ):  # switch to class  method of concrete type
                return getattr(concrete_type, x.__name__)
            return x  # leave as is

        clone.map_from_string = remap_constructor_from_super_type_to_concrete_type(
            clone.map_from_string
        )
        clone.map_from_json = remap_constructor_from_super_type_to_concrete_type(
            clone.map_from_json
        )
        clone.map_from_db = remap_constructor_from_super_type_to_concrete_type(clone.map_from_db)
        return clone

    def _set_string_defaults(self) -> None:
        if self.db_type is None:
            self.db_type = str
        if self.json_type is None:
            self.json_type = str
        self.map_from_string = passthru_none(self.get_type(), if_none=self.map_from_string)
        self.map_to_string = passthru_none(str, if_none=self.map_to_string)
        self.map_from_json = passthru_none(self.map_from_string, if_none=self.map_from_json)
        self.map_to_json = passthru_none(self.map_to_string, if_none=self.map_to_json)
        self.map_from_db = passthru_none(self.map_from_string, if_none=self.map_from_db)
        self.map_to_db = passthru_none(self.map_to_string, if_none=self.map_to_db)

    def _set_db_defaults(self) -> None:
        assert self.db_type is not None and self.map_from_db is not None
        self.map_to_db = passthru_none(self.db_type, if_none=self.map_to_db)
        if self.db_type is bytes:
            if self.json_type is None:
                self.json_type = str
            self.map_from_json = passthru_none(ensure_bytes, if_none=self.map_from_json)
            self.map_to_json = passthru_none(encode_base64, if_none=self.map_to_json)
            self.map_from_string = passthru_none(
                ensure_bytes, self.map_from_db, if_none=self.map_from_string
            )
            self.map_to_string = passthru_none(
                self.map_to_db, encode_base64, if_none=self.map_to_string
            )
        else:
            if self.json_type is None:
                self.json_type = self.db_type
            self.map_from_json = passthru_none(self.map_from_db, if_none=self.map_from_json)
            self.map_to_json = passthru_none(self.map_to_db, if_none=self.map_to_json)
            self.map_from_string = passthru_none(
                self.db_type, self.map_from_db, if_none=self.map_from_string
            )
            self.map_to_string = passthru_none(self.map_to_db, str, if_none=self.map_to_string)

    def _set_json_defaults(self) -> bool:
        if self.map_from_json is not None and self.map_to_json is not None:
            if self.db_type is None:
                self.db_type = str
            if self.json_type is None:
                self.json_type = dict
            self.map_from_json = passthru_none(self.map_from_json)
            self.map_to_json = passthru_none(self.map_to_json)
            self.map_from_string = passthru_none(
                json_loads, self.map_from_json, if_none=self.map_from_string
            )
            self.map_to_string = passthru_none(
                self.map_to_json, json_dumps, if_none=self.map_to_string
            )
            self.map_from_db = passthru_none(self.map_from_string, if_none=self.map_from_db)
            self.map_to_db = passthru_none(self.map_to_string, if_none=self.map_to_db)
            return True
        return False

    def resolve_the_rest(self) -> Self:
        assert not self.is_factory, "factory types cannot resolve the rest"
        if self.concrete_type is not None:
            assert self.abstract_type is None
            if self.map_from_string is not None or self.map_to_string is not None:
                self._set_string_defaults()
            else:
                if self.db_type is None and DbTypeInfo.is_db_type(self.concrete_type):  # pyright: ignore
                    self.db_type = self.concrete_type
                if self.db_type is not None:
                    self.map_from_db = passthru_none(
                        self.db_type, self.concrete_type, if_none=self.map_from_db
                    )
                    self._set_db_defaults()
                else:
                    self._set_json_defaults()

        else:
            assert self.abstract_type is not None
            assert (
                self.map_from_string is not None
                or self.map_from_json is not None
                or self.map_from_db is not None
            )
            if self.map_from_string is not None:
                self._set_string_defaults()
            elif self._set_json_defaults():
                pass
            elif self.db_type is not None:
                assert self.map_from_db is not None
                self.map_from_db = passthru_none(self.map_from_db)
                self._set_db_defaults()

        return self

JsonBase

Bases: BaseModel

Source code in src/lythonic/types.py
class JsonBase(BaseModel):
    type_gref: GRef | None = None

    @model_validator(mode="before")
    @classmethod
    def set_gref(cls, data: dict[str, Any]) -> dict[str, Any]:
        data["type_gref"] = str(GlobalRef(cls))
        return data

    @classmethod
    def from_json(cls, json: dict[str, Any]) -> Self:
        if "type_gref" in json:
            ref = GlobalRef(json["type_gref"])
            cls_from_ref = ref.get_instance()
            if issubclass(cls_from_ref, cls):
                return cls_from_ref.model_validate(json)
        return cls.model_validate(json)

    def to_json(self) -> dict[str, Any]:
        return base_model_to_json(self)

DbTypeInfo

Bases: Enum

Source code in src/lythonic/types.py
class DbTypeInfo(Enum):
    def __init__(self, input_types: tuple[type, ...], target_type: type):
        assert isinstance(input_types, tuple), f"input_types must be tuple, not {type(input_types)}"
        cls = self.__class__
        if not hasattr(cls, "_value2member_map_"):
            cls._value2member_map_ = {}
        member_map = cls._value2member_map_
        for k in input_types:
            assert k not in member_map, f"Duplicate input type {k} in {self.name}"
            member_map[k] = self
        self.input_types = input_types
        self.target_type = target_type

    INTEGER = ((int, bool), int)
    REAL = ((float,), float)
    TEXT = ((str,), str)
    BLOB = ((bytes,), bytes)

    @classmethod
    def from_type(cls, t: type) -> DbTypeInfo:
        return cast(
            DbTypeInfo,
            cls._value2member_map_[t],
        )

    @classmethod
    def is_db_type(cls, type_: type) -> bool:
        """could be used in db as it is"""
        return type_ in cls._value2member_map_

is_db_type(type_) classmethod

could be used in db as it is

Source code in src/lythonic/types.py
@classmethod
def is_db_type(cls, type_: type) -> bool:
    """could be used in db as it is"""
    return type_ in cls._value2member_map_

MapPair

Bases: Generic[T]

Source code in src/lythonic/types.py
class MapPair(Generic[T]):
    target_type: type
    map_from: Callable[[T], Any]
    map_to: Callable[[Any], T]

    def __init__(
        self, map_from: Callable[[T], Any], map_to: Callable[[Any], T], target_type: type
    ) -> None:
        self.map_from = map_from
        self.map_to = map_to
        self.target_type = target_type