I have a metaclass which creates a generic class parametrized by a type var T
.
It also needs to know the same type at runtime, which I currently achieve by providing it as an additional argument.
Both types must be the same always.
Currently, when creating an instance class, I have to specify the type twice, once for the type checker and once for runtime use. I would like to avoid this. I think as generic arguments should not be used at runtime, the solution would be to infer the generic type from the metaclass argument. Is that even possible? Or is there another solution, that allows to specify the type just once?
MWE:
class Meta(type):
def __new__(
cls,
name: str,
bases: tuple[type, ...],
cls_dict: dict[str, object],
wrapped_class: type,
):
for member in wrapped_class.__dict__:
if member.startswith("__"): # exclude python internals
continue
if member not in cls_dict:
raise TypeError(f"Need to implement {member}")
return super().__new__(cls, name, bases, cls_dict)
class Base:
def foo(self):
return "bar"
class Wrapper[T](metaclass=Meta, wrapped_class=object):
def do_wrapper_stuff(self, arg: T) -> T:
return arg
class BaseWrapper(Wrapper[Base], wrapped_class=Base): ... # TypeError: "Need to implement foo" – as is intended
More context what I am trying to achieve: I have multiple classes for which I am writing wrappers. The wrappers need to always have parity with the wrapped classes, because either could be given as parameters to other functions. The metaclass is supposed to ensure this. The generic argument is needed for the type checker to know which class is wrapped.
I am aware that subclassing the wrapped class would achieve about that. However, when the wrapped class obtains new attributes, they must not be accessible by default (the wrapper is part of a permission system), instead the wrapper should raise an exception when trying to access them (or implement the intended behaviour). This is very hard to detect when the wrapper is a subclass, so my current approach is to implement it separately (the code needs to be written in either case) and perform a type cast to the wrapped class for further use.
You don't have to specify it twice, although you do need some strategies to narrow the wrapped class down to a single item, which can get complicated when you declare a subclass with lots of base classes being parameterised with type arguments.
I don't know your subclass usage well enough, but here's something that can get you started.
from typing import Any, Generic
class Meta(type):
def __new__[Self: Meta](
cls: type[Self], name: str, bases: tuple[type, ...], cls_dict: dict[str, Any]
) -> Self:
# Handles `class Wrapper[T](metaclass=Meta): ...`
# The `[T]` implicitly inserts a `typing.Generic` in your base classes
if bases[0] is Generic: # type: ignore[comparison-overlap]
return super().__new__(cls, name, bases, cls_dict)
# `__orig_bases__` is the exact tuple of base class expressions that you declared the class with.
# In your example, it is `(Wrapper[Base],)`.
orig_bases: tuple[Any, ...] | None = cls_dict.get("__orig_bases__")
if orig_bases is not None:
# See e.g. https://stackoverflow.com/questions/48572831
wrapped_class = orig_bases[0].__args__[0]
else:
# If you don't parameterise any generics (e.g. `class BaseWrapper(Wrapper): ...`, then this tuple doesn't exist.
return super().__new__(cls, name, bases, cls_dict)
for member in wrapped_class.__dict__:
if member.startswith("__"): # exclude python internals
continue
if member not in cls_dict:
raise TypeError(f"Need to implement {member}")
return super().__new__(cls, name, bases, cls_dict)
class Base:
def foo(self) -> str:
return "bar"
class Wrapper[T](metaclass=Meta):
def do_wrapper_stuff(self, arg: T) -> T:
return arg
class BaseWrapper(Wrapper[Base]): ... # TypeError: Need to implement foo