Here's the situation. I'm trying to reduce the amount of boilerplate code in my project. Here is a simplified version of the setup.
from __future__ import annotations
import dataclasses
import typing as t
import types
_Self = t.TypeVar("_Self", bound="A")
class A:
"""Base class for all classes"""
@classmethod
def create(cls: type[_Self], *args, **kwargs) -> _Self:
return cls(*args, **kwargs)
def do_something(self) -> None:
"""a 'void' method"""
pass
class B(A):
"""A secondary base class for the future dataclasses; NOTE: inheritance from A!"""
T = t.TypeVar("T", bound=B)
TCallable = t.Callable[[type], type[T]]
def decorator(**kwargs) -> TCallable:
"""A decorator for reducing boilerplate"""
def _decorator(cls: type) -> type[B]:
return types.new_class(
cls.__name__,
(
dataclasses.dataclass(**{"frozen": True, "kw_only": True, **kwargs}(cls),
B,
),
)
return _decorator
@decorator(repr=False)
class Test:
"""This is an implementation of the dataclass, subclassed from B, subclassed from A"""
name: str
def __repr__(self) -> str:
return self.name
def do_something_else(self) -> None:
self.do_something() # <- not recognized by pyright
The problem is that my static type checker is unable to recognize the methods from class A
at the top of the resolution order from within instances of class Test
.
I'm using Pylance/Pyright in VSCode.
Edited to correct error in decorator
First, I should note it can be implemented without the decorator like so:
@dataclasses.dataclass(frozen = True, kw_only = True, repr=False)
class Test(B):
"""This is an implementation of the dataclass, subclassed from B, subclassed from A"""
name: str
def __repr__(self) -> str:
return self.name
def do_something_else(self) -> None:
self.do_something()
But let's assume, that for some reason it needs to be there. This example assumes that you just wanted to avoid typing frozen
and kw_only
many times:
from __future__ import annotations
import dataclasses
import typing as t
_Self = t.TypeVar("_Self", bound="A")
class A:
"""Base class for all classes"""
@classmethod
def create(cls: type[_Self], *args, **kwargs) -> _Self:
return cls(*args, **kwargs)
def do_something(self) -> None:
"""a 'void' method"""
pass
class B(A):
"""A secondary base class for the future dataclasses; NOTE: inheritance from A!"""
@t.dataclass_transform()
def decorator(**kwargs):
def _decorator(cls: type) -> type[B]:
return dataclasses.dataclass(**{"frozen": True, "kw_only": True, **kwargs})(cls)
return _decorator
@decorator(repr=False)
class Test(B):
"""This is an implementation of the dataclass, subclassed from B, subclassed from A"""
name: str
def __repr__(self) -> str:
return self.name
def do_something_else(self) -> None:
self.do_something()
The inheritance aspect is impossible to add into the type currently, which is why I've left it inheriting from B. In general this is probably better practice anyway, as it makes the inheritance structure clearer. However, this may eventually be possible to type with Intersection
. Hope this is useful!