Skip to content

Commit

Permalink
[pythongh-119581] Add a test of InitVar with name shadowing
Browse files Browse the repository at this point in the history
As originally discussed in
python/mypy#17219,
MyPy has had a false-positive bug report because it errors when a
dataclass has methods that shadow an `InitVar` field.

It is actually a bit surprising that this works, it turns out
that `__annotations__` "remembers" field assignments even if the
bound names are later overwritten by methods; it will *not* work
to provide a default value.

There have been multiple bug reports on MyPy so we know people are
actually relying on this in practice; most likely it comes up when
a dataclass wants to take a "raw" value as an InitVar and transform
it somehow in `__post_init__` into a different value before assigning
it to a field; in that case they may choose to make the actual field
private and provide a property for access.

I currently provide a test of the happy path where there is no default
value provided, but no tests of the behavior when no default is
provided (in which case the property will override the default) and no
documentation (because I'm not sure we want to consider this behavior
officially supported).

The main goal is to have a regression test since it would be easy for a
refactor to break this.
  • Loading branch information
stroxler committed May 26, 2024
1 parent 0220663 commit 2f5b3b8
Showing 1 changed file with 23 additions and 0 deletions.
23 changes: 23 additions & 0 deletions Lib/test/test_dataclasses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1317,6 +1317,29 @@ def __post_init__(self, init_base, init_derived):
c = C(10, 11, 50, 51)
self.assertEqual(vars(c), {'x': 21, 'y': 101})

def test_init_var_name_shadowing(self):
# Because dataclasses rely exclusively on `__annotations__` for
# handling InitVar and `__annotations__` preserves shadowed definitions,
# you can actually shadow an InitVar with a method or property.
#
# This only works when there is no default value; `dataclasses` uses the
# actual name (which will be bound to the shadowing method) for default
# values.
@dataclass
class C:
shadowed: InitVar[int]
_shadowed: int = field(init=False)

def __post_init__(self, shadowed):
self._shadowed = shadowed

@property
def shadowed(self):
return self._shadowed

c = C(5)
self.assertEqual(c.shadowed, 5)

def test_default_factory(self):
# Test a factory that returns a new list.
@dataclass
Expand Down

0 comments on commit 2f5b3b8

Please sign in to comment.