PEP560 introduced cls.__orig_bases__
and the typing
module has the utility functions get_origin
and get_args
that allow you to extract the type; however, when you have a (complex) class hierarchy with multiple levels of generic inheritance then you need to iterate over that hierarchy and push the concrete type parameters down through the hierarchy.
For example, if you have the classes:
from typing import Generic, Literal, LiteralString
from datetime import date
T = TypeVar("T")
L = TypeVar("L", bound=LiteralString)
F = TypeVar("F", bound=str)
class Thing(Generic[T, L]):
pass
class ChildThing(Thing[T, L], Generic[T, L]):
pass
class StringThing(ChildThing[T, Literal["string"]], Generic[F, T]):
pass
class DateThing(StringThing[Literal["date"], date]):
pass
Then:
print(get_args(DateThing.__orig_bases__[0]))
Will output:
(typing.Literal['date'], <class 'datetime.date'>)
And will not give you the type of the parameter T
for the base Thing
class. For that you need more complicated logic:
from typing import (
Any,
Dict,
Generic,
Literal,
LiteralString,
Optional,
Tuple,
TypeVar,
get_args,
get_origin,
)
from datetime import date
T = TypeVar("T")
L = TypeVar("L", bound=LiteralString)
F = TypeVar("F", bound=str)
def get_generic_map(
base_cls: type,
instance_cls: type,
) -> Dict[Any, Any]:
"""Get a map from the generic type paramters to the non-generic implemented types.
Args:
base_cls: The generic base class.
instance_cls: The non-generic class that inherits from ``base_cls``.
Returns:
A dictionary mapping the generic type parameters of the base class to the
types of the non-generic sub-class.
"""
assert base_cls != instance_cls
assert issubclass(instance_cls, base_cls)
cls: Optional[type] = instance_cls
generic_params: Tuple[Any, ...]
generic_values: Tuple[Any, ...] = tuple()
generic_map: Dict[Any, Any] = {}
# Iterate over the class hierarchy from the instance sub-class back to the base
# class and push the non-generic type paramters up through that hierarchy.
while cls is not None and issubclass(cls, base_cls):
if hasattr(cls, "__orig_bases__"):
# Generic class
bases = cls.__orig_bases__
# Get the generic type parameters.
generic_params = next(
(
get_args(generic)
for generic in bases
if get_origin(generic) is Generic
),
tuple(),
)
# Generate a map from the type parameters to the non-generic types pushed
# from the previous sub-class in the hierarchy.
generic_map = (
{param: value for param, value in zip(generic_params, generic_values)}
if len(generic_params) > 0
else {}
)
# Use the type map to push the concrete parameter types up to the next level
# of the class hierarchy.
generic_values = next(
(
tuple(
generic_map[arg] if arg in generic_map else arg
for arg in get_args(base)
)
for base in bases
if (
isinstance(get_origin(base), type)
and issubclass(get_origin(base), base_cls)
)
),
tuple(),
)
else:
generic_map = {}
assert isinstance(cls, type)
cls = next(
(base for base in cls.__bases__ if issubclass(base, base_cls)),
None,
)
return generic_map
class Thing(Generic[T, L]):
type: T
literal: L
def __init__(self) -> None:
super().__init__()
type_map = get_generic_map(Thing, type(self))
self.type = type_map[T]
self.literal = type_map[L]
def hello(self) -> str:
return f"I am type={self.type}, literal={self.literal}"
class ChildThing(Thing[T, L], Generic[T, L]):
pass
class StringThing(ChildThing[T, Literal["string"]], Generic[F, T]):
def __init__(self) -> None:
super().__init__()
type_map = get_generic_map(StringThing, type(self))
self.f = type_map[F]
def hello(self) -> str:
return f"{super().hello()}, f={self.f}"
class DateThing(StringThing[Literal["date"], date]):
pass
thing = DateThing()
print(thing.hello())
Which outputs:
I am type=<class 'datetime.date'>, literal=typing.Literal['string'], f=typing.Literal['date']
type
method? e.g.my_type = type(T)
? – Aparri