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)
:
# 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".