I would like to use the new Python 3.12 generic type signature syntax to know the type of an about-to-be-instantiated class within the classmethod of that class.
For example, I would like to print the concrete type of T
in this example:
class MyClass[T]:
kind: type[T]
...
@classmethod
def make_class[T](cls) -> "MyClass[T]":
print("I am type {T}!")
return cls()
I’ve used the advice in the following StackOverflow question to do this for reifying the type within both __new__
and __init__
but I have yet to to figure out a clever way to do this in either a static or class method (ideally a class method).
- Generic[T] base class – how to get type of T from within instance?
My goal is to have the following API:
>>> MyClass[int].make_class()
"I am type int!"
Or this API (which I don’t think is syntactically possible yet):
>>> MyClass.make_class[int]()
"I am type int!"
Where in either case, the returned instance would have int
bound to the class variable so I can use it later.
MyClass[int].make_class().kind is int == True
I am open to “hacks” (including heavy use of inspect
).
If you read the source code of typing.py
, you’ll see that the type arguments for a Generic
is stored as the __args__
attribute of the base class _BaseGenericAlias
. Note that this is an undocumented implementation detail.
We then just need to patch its proxy attribute getter _BaseGenericAlias.__getattr__
to inject an additional keyword argument into the method call with a wrapper function when the method is decorated with a classmethod
subclass that marks the function with a special attribute:
class TypeArgsClassMethod(classmethod):
def __get__(self, obj, obj_type=None):
method = super().__get__(obj, obj_type)
method.__func__._inject_type_args = True
return method
def __getattr__(self, name):
if hasattr(obj := orig_getattr(self, name), '_inject_type_args'):
@wraps(obj)
def wrapper(*args, **kwargs):
return obj(*args, __type_args__=self.__args__, **kwargs)
return wrapper
return obj
if 'orig_getattr' not in globals():
orig_getattr = _BaseGenericAlias.__getattr__
_BaseGenericAlias.__getattr__ = __getattr__
so that:
class MyClass[T]:
kind: type[T]
@TypeArgsClassMethod
def make_class(cls, __type_args__) -> "MyClass[T]":
print(__type_args__)
return cls()
MyClass[int].make_class()
outputs:
(<class 'int'>,)
Demo here
Alternatively, you can rebind the custom class method to the Generic
type, so that the first argument of the class method would become the Generic
type. The built-in types.MethodType
does not allow __self__
to be updated for a rebind so you would have to define a Python-equivalent version:
from functools import update_wrapper
from typing import _BaseGenericAlias
class MethodType:
def __init__(self, func, obj):
self.__func__ = func
self.__self__ = obj
def __call__(self, *args, **kwargs):
func = self.__func__
obj = self.__self__
return func(obj, *args, **kwargs)
class GenericClassMethod:
def __init__(self, f):
self.f = f
update_wrapper(self, f)
def __get__(self, obj, cls=None):
if cls is None:
cls = type(obj)
method = MethodType(self.f, cls)
method._generic_classmethod = True
return method
def __getattr__(self, name):
if hasattr(obj := orig_getattr(self, name), '_generic_classmethod'):
obj.__self__ = self
return obj
if 'orig_getattr' not in globals():
orig_getattr = _BaseGenericAlias.__getattr__
_BaseGenericAlias.__getattr__ = __getattr__
so that you can access its __args__
attribute for the type arguments, and its __origin__
attribute for the original class:
class MyClass[T]:
kind: type[T]
@GenericClassMethod
def make_class(cls) -> "MyClass[T]":
print(cls)
print(cls.__origin__)
print(cls.__args__)
MyClass[int].make_class()
This outputs:
__main__.MyClass[int]
<class '__main__.MyClass'>
(<class 'int'>,)
Demo here
Finally, an even more seamless approach that would achieve the behavior of substiting the type variable T
with the actual argument as suggested in your question would be to map the type variables the in cell content of the class method’s function closure to the corresponding type arguments.
This approach has the benefit of avoiding any alteration to the class method’s usage (with no injection of an argument and no change to the nature of the cls
argument) so you can avoid having to define a custom classmethod
descriptor. To identify a class method you can use the isclassmethod
function offered in this answer:
from typing import _BaseGenericAlias
from types import FunctionType, CellType
from collections.abc import Hashable
from operator import attrgetter
def __getattr__(self, name):
if (isclassmethod(obj := orig_getattr(self, name)) and
(func := obj.__func__).__closure__):
param_to_arg = dict(zip(self.__origin__.__parameters__, self.__args__))
return FunctionType(
func.__code__,
func.__globals__,
func.__name__,
func.__defaults__,
tuple(
CellType(
param_to_arg.get(value, value)
if isinstance(value, Hashable) else value
)
for value in map(attrgetter('cell_contents'), func.__closure__)
)
).__get__(obj.__self__)
return obj
if 'orig_getattr' not in globals():
orig_getattr = _BaseGenericAlias.__getattr__
_BaseGenericAlias.__getattr__ = __getattr__
so that:
class MyClass[T]:
@classmethod
def make_class(cls):
print(T)
MyClass[int].make_class()
outputs:
<class 'int'>
Demo here
8