typing
typing copied to clipboard
Merging/Modifying Types
I have a decorator that modifies a class at runtime, kind of like a dataclass:
@modify
class Something:
...
Let's say modify
adds foo
onto Something
, so after calling modify
you could use it like so:
@modify
class Something
...
bar = Something.foo()
dataclass_transform
doesn't support this kind of behavior at all. So, how would an API like this work?
I don't see why it would be difficult to implement some sort of Merge
type, that takes in a type and "applies"" a Protocol
to it. For the above example, it would work like this:
T = TypeVar("T")
class FooProtocol(Protocol):
@classmethod
def foo() -> None:
...
def modify(tp: type[T]) -> Merge[T, FooProtocol]:
tp.foo = ... # type: ignore
return tp
@modify
class Something:
...
bar = Something.foo()
In the above example, ideally it would just sort of mark that the T
is now compatible with FooProtocol
, but a type: ignore
is used inside of modify
because it would probably be a mess to try and come up with some sort of API for actually converting a T
to Merge[T, ...]
.
TL;DR A type that marks another type as being compatible with a protocol could be handy for making dataclass-like decorators.
What you're looking for here is an "intersection type". It's the converse of a "union type". PEP 483 contemplated adding intersections to the type system but deferred it until later. The idea has recently been revived, but the feature is not easy to spec or implement.
In the meantime, let's see if there's a workaround that suits your needs.
I'll note that your class decorator is directly mutating the class it's decorating, which is generally not something I would recommend doing. It would be better to create a new class object and modify it rather than directly mutating the original class. This can lead to surprises if the decorator is applied separately.
class SomethingWithoutFoo: ...
SomethingWithFoo = modify(SomethingWithoutFoo) # This mutates the original SomethingWithoutFoo
Have you considered using a mix-in class rather than a decorator? The mix-in design pattern is well established and supported in Python, and static type checkers can reason about it. It's no more verbose than a decorator, and it doesn't require any fragile mutations. It's also safer because a static type checker can tell you if you're inadvertently overriding an attribute in an incompatible manner (e.g. if the Something
class happens to have an attribute named foo
that is used for some other purpose).
class FooMixin:
@classmethod
def foo(cls) -> None:
...
class Something(FooMixin):
pass
I have considered mixins, but it's not exactly ideal when you want to do more with the class than just modify it. For example, with a dataclass-like situation, I don't think the following would be very clear:
class Foo(FooDataclass):
a: str
b: str
I guess you could resort to something like:
@modify
class Foo(FooMixin):
a: str
b: str
But in my eyes that's too much boilerplate (especially if you're planning on doing it a lot). It is subjective, though. Is there a PEP on intersections yet?
I have posted something similar, it would help during dynamic class overloads #1511