Suppose I want to create an alias for a dataclasses.dataclass
decorator with specific arguments. For example:
# Instead of repeating this decorator all the time:
@dataclasses.dataclass(frozen=True, kw_only=True)
class Entity:
...
# I just write something like this:
@struct
class Entity:
...
The static analyzer I am using is Pylance, in Visual Studio Code.
I am using Python 3.11.
My first instinct was to leverage the fact that functions are first-class citizens and simply assign the created decorator function to a custom name. This works at runtime, but Pylance no longer recognizes Entity
as a dataclass, as evident from the static analysis error:
struct = dataclasses.dataclass(frozen=True, kw_only=True)
@struct
class Entity:
name: str
value: int
# STATIC ANALYZER:
# Expected no arguments to "Entity" constructor Pylance(reportCallIssue)
valid_entity = Entity(name="entity", value=42)
# RUNTIME:
# Entity(name='entity', value=42)
print(valid_entity)
I then thought that maybe some information was being lost somehow if I just assign to another name (though I don't see why that would be the case), so I looked to wrapping it with functools. However, this still has the same behavior in static analysis and even causes a runtime error, when I apply @struct
:
import dataclasses
import functools
def struct(cls):
decorator = dataclasses.dataclass(frozen=True, kw_only=True)
decorated_cls = decorator(cls)
functools.update_wrapper(decorated_cls, cls)
return decorated_cls
# No error reported by static analyzer, but runtime error at `@struct`:
# AttributeError: 'mappingproxy' object has no attribute 'update'
@struct
class Entity:
name: str
value: int
# STATIC ANALYZER:
# Expected no arguments to "Entity" constructor Pylance(reportCallIssue)
# RUNTIME:
# (this line doesn't even get reached)
valid_entity = Entity(name="entity", value=42)
Full traceback:
Traceback (most recent call last):
File "C:\Users\***\temp.py", line 12, in <module>
@struct
^^^^^^
File "C:\Users\***\temp.py", line 7, in struct
functools.update_wrapper(decorated_cls, cls)
File "C:\Users\***\AppData\Local\Programs\Python\Python311\Lib\functools.py", line 58, in update_wrapper
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'mappingproxy' object has no attribute 'update'
I then tried making struct
a decorator factory instead and used functools.wraps()
on a closure function that just forwards to the dataclass function. This now works at runtime, but Pylance still reports the same error as in Attempt 1:
def struct():
decorator = dataclasses.dataclass(frozen=True, kw_only=True)
@functools.wraps(decorator)
def decorator_wrapper(*args, **kwargs):
return decorator(*args, **kwargs)
return decorator_wrapper
@struct()
class Entity:
name: str
value: int
# STATIC ANALYZER:
# Expected no arguments to "Entity" constructor Pylance(reportCallIssue)
valid_entity = Entity(name="entity", value=42)
# RUNTIME:
# Entity(name='entity', value=42)
print(valid_entity)
I also found that using the plain dataclasses.dataclass
function itself (no ()
) has the exact same problem across all 3 attempts.
Is there any way to get this to work without messing up IntelliSense?
Optional follow-up: why did Attempt 2 fail at runtime?
Decorate struct()
with dataclass_transform(frozen_default = True, kw_only_default = True)
:
(playgrounds: Mypy, Pyright)
# 3.11+
from typing import dataclass_transform
# 3.10-
from typing_extensions import dataclass_transform
@dataclass_transform(frozen_default = True, kw_only_default = True)
def struct[T](cls: type[T]) -> type[T]:
return dataclass(frozen = True, kw_only = True)(cls)
# By the way, you can actually pass all of them
# to dataclass() in just one call:
# dataclass(cls, frozen = True, kw_only = True)
# It's just that this signature isn't defined statically.
@struct
class Entity:
name: str
value: int
reveal_type(Entity.__init__) # (self: Entity, *, name: str, value: int) -> None
valid_entity = Entity(name="entity", value=42) # fine
valid_entity.name = "" # error: "Entity" is frozen
dataclass_transform()
is used to mark dataclass transformers (those that has similar behaviour to the built-in dataclasses.dataclass
). It accepts a number of keyword arguments, in which:
frozen_default = True
means that the class decorated with @struct
will be frozen "by default".kw_only_default = True
means that the constructor generated will only have keyword arguments (aside from self
) "by default"."By default" means that, unless otherwise specified via the frozen
/kw_only
arguments to the @struct
decorator, the @struct
-decorated class will behave as such. However, since struct
itself takes no such arguments, "by default" here is the same as "always".
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With