'How can I automatically apply a class decorator to all subclasses?
I have the following code:
from dataclasses import dataclass
class Expr:
def __repr__(self):
fields = ', '.join(f'{v!r}' for v in self.__dict__.values())
return f'{self.__class__.__name__}({fields})'
@dataclass(repr=False)
class Literal(Expr):
value: object
@dataclass(repr=False)
class Binary(Expr):
lhs: Expr
op: str
rhs: Expr
This code allows me to define expression trees and to match against them, e.g.:
def fmt_ast(expr: Expr) -> str:
match expr:
case Literal(x):
return str(x)
case Binary(lhs, op, rhs):
return f'({op} {fmt_ast(lhs)} {fmt_ast(rhs)})'
assert False, 'Unreachable'
expr = Binary(Literal(42), '+', Literal(23))
print(fmt_ast(expr))
# (+ 42 23)
However, I would like to avoid having to decorate each Expr subclass manually. I.e. I want to skip writing @dataclass(repr=False). Based on another answer, I rewrote Expr as follows:
class Expr:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
dataclass(repr=False)(cls)
def __repr__(self):
fields = ', '.join(f'{v!r}' for v in self.__dict__.values())
return f'{self.__class__.__name__}({fields})'
… and this seems to work (even after removing the decorators from the subclasses). The thing is, I don’t understand why it works: sure, I’m invoking the dataclass decorator but I’m not doing anything with the result, i.e.with the decorated type. I don’t assign it to anything (to what?!) and I don’t return it, since __init_subclass__ has no meaningful return value. And yet, the code continues to run as if the subclasses had been decorated with dataclass.
However, mypy (0.942) calls my bluff:
test/__init__.py:29: error: Class "test.Literal" doesn't define "__match_args__" test/__init__.py:31: error: Class "test.Binary" doesn't define "__match_args__" test/__init__.py:36: error: Too many arguments for "Binary" test/__init__.py:36: error: Too many arguments for "Literal"
I believe that mypy is correct, which leaves me with two questions:
- Why does this seem to work at all?
- How to do this properly, i.e. in a way that satisfies
mypy, without reimplementing thedataclassfunctionality from scratch insideExpror its metaclass?
Solution 1:[1]
@juanpa.arrivillaga has explained in the comments perfectly what is taking place, as put:
- a
dataclasscall modifies the class in place: the same object is mutated so that it gains the new methods and new behaviors. mypycan't know about the new methods, because it does not run the code: it special cases thedataclassdecorator, so that in ordinary uses it does know about the new attributes. The workaround there would be adding the methods that dataclass create toReprso that mypy would be fooled.- some calls to dataclass might return a new object, like when using the new
slotsparameter, in that case,__init_subclass__won't help, as dataclass returns a new class, built with base on the one that was passed to it. For the same approach to work, you will have to use a metaclass, and override its__new__method, instead (or along with) ordinary inheritance. Themetaclass.__new__method has to return the actual object that will be used as the class.
def __repr__(self):
# ...
# stand alone method which will be injected by the metaclass
class MExpr(type):
def __new__(mcls, name, bases, ns):
cls = super().__new__(mcls, name, bases, ns)
cls.__repr__ = __repr__
return dataclass(repr=False, slots=...)(cls)
class Literal(metaclass=MExpr):
...
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | jsbueno |
