Skip to content

Commit e7adf2b

Browse files
authored
bpo-33796: Ignore ClassVar for dataclasses.replace(). (GH-7488)
1 parent 34b7346 commit e7adf2b

File tree

2 files changed

+125
-86
lines changed

2 files changed

+125
-86
lines changed

Lib/dataclasses.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ def _field_init(f, frozen, globals, self_name):
416416
# Only test this now, so that we can create variables for the
417417
# default. However, return None to signify that we're not going
418418
# to actually do the assignment statement for InitVars.
419-
if f._field_type == _FIELD_INITVAR:
419+
if f._field_type is _FIELD_INITVAR:
420420
return None
421421

422422
# Now, actually generate the field assignment.
@@ -1160,6 +1160,10 @@ class C:
11601160
# If a field is not in 'changes', read its value from the provided obj.
11611161

11621162
for f in getattr(obj, _FIELDS).values():
1163+
# Only consider normal fields or InitVars.
1164+
if f._field_type is _FIELD_CLASSVAR:
1165+
continue
1166+
11631167
if not f.init:
11641168
# Error if this field is specified in changes.
11651169
if f.name in changes:

Lib/test/test_dataclasses.py

+120-85
Original file line numberDiff line numberDiff line change
@@ -1712,91 +1712,6 @@ class Parent(Generic[T]):
17121712
# Check MRO resolution.
17131713
self.assertEqual(Child.__mro__, (Child, Parent, Generic, object))
17141714

1715-
def test_helper_replace(self):
1716-
@dataclass(frozen=True)
1717-
class C:
1718-
x: int
1719-
y: int
1720-
1721-
c = C(1, 2)
1722-
c1 = replace(c, x=3)
1723-
self.assertEqual(c1.x, 3)
1724-
self.assertEqual(c1.y, 2)
1725-
1726-
def test_helper_replace_frozen(self):
1727-
@dataclass(frozen=True)
1728-
class C:
1729-
x: int
1730-
y: int
1731-
z: int = field(init=False, default=10)
1732-
t: int = field(init=False, default=100)
1733-
1734-
c = C(1, 2)
1735-
c1 = replace(c, x=3)
1736-
self.assertEqual((c.x, c.y, c.z, c.t), (1, 2, 10, 100))
1737-
self.assertEqual((c1.x, c1.y, c1.z, c1.t), (3, 2, 10, 100))
1738-
1739-
1740-
with self.assertRaisesRegex(ValueError, 'init=False'):
1741-
replace(c, x=3, z=20, t=50)
1742-
with self.assertRaisesRegex(ValueError, 'init=False'):
1743-
replace(c, z=20)
1744-
replace(c, x=3, z=20, t=50)
1745-
1746-
# Make sure the result is still frozen.
1747-
with self.assertRaisesRegex(FrozenInstanceError, "cannot assign to field 'x'"):
1748-
c1.x = 3
1749-
1750-
# Make sure we can't replace an attribute that doesn't exist,
1751-
# if we're also replacing one that does exist. Test this
1752-
# here, because setting attributes on frozen instances is
1753-
# handled slightly differently from non-frozen ones.
1754-
with self.assertRaisesRegex(TypeError, r"__init__\(\) got an unexpected "
1755-
"keyword argument 'a'"):
1756-
c1 = replace(c, x=20, a=5)
1757-
1758-
def test_helper_replace_invalid_field_name(self):
1759-
@dataclass(frozen=True)
1760-
class C:
1761-
x: int
1762-
y: int
1763-
1764-
c = C(1, 2)
1765-
with self.assertRaisesRegex(TypeError, r"__init__\(\) got an unexpected "
1766-
"keyword argument 'z'"):
1767-
c1 = replace(c, z=3)
1768-
1769-
def test_helper_replace_invalid_object(self):
1770-
@dataclass(frozen=True)
1771-
class C:
1772-
x: int
1773-
y: int
1774-
1775-
with self.assertRaisesRegex(TypeError, 'dataclass instance'):
1776-
replace(C, x=3)
1777-
1778-
with self.assertRaisesRegex(TypeError, 'dataclass instance'):
1779-
replace(0, x=3)
1780-
1781-
def test_helper_replace_no_init(self):
1782-
@dataclass
1783-
class C:
1784-
x: int
1785-
y: int = field(init=False, default=10)
1786-
1787-
c = C(1)
1788-
c.y = 20
1789-
1790-
# Make sure y gets the default value.
1791-
c1 = replace(c, x=5)
1792-
self.assertEqual((c1.x, c1.y), (5, 10))
1793-
1794-
# Trying to replace y is an error.
1795-
with self.assertRaisesRegex(ValueError, 'init=False'):
1796-
replace(c, x=2, y=30)
1797-
with self.assertRaisesRegex(ValueError, 'init=False'):
1798-
replace(c, y=30)
1799-
18001715
def test_dataclassses_pickleable(self):
18011716
global P, Q, R
18021717
@dataclass
@@ -3003,6 +2918,126 @@ def test_funny_class_names_names(self):
30032918
C = make_dataclass(classname, ['a', 'b'])
30042919
self.assertEqual(C.__name__, classname)
30052920

2921+
class TestReplace(unittest.TestCase):
2922+
def test(self):
2923+
@dataclass(frozen=True)
2924+
class C:
2925+
x: int
2926+
y: int
2927+
2928+
c = C(1, 2)
2929+
c1 = replace(c, x=3)
2930+
self.assertEqual(c1.x, 3)
2931+
self.assertEqual(c1.y, 2)
2932+
2933+
def test_frozen(self):
2934+
@dataclass(frozen=True)
2935+
class C:
2936+
x: int
2937+
y: int
2938+
z: int = field(init=False, default=10)
2939+
t: int = field(init=False, default=100)
2940+
2941+
c = C(1, 2)
2942+
c1 = replace(c, x=3)
2943+
self.assertEqual((c.x, c.y, c.z, c.t), (1, 2, 10, 100))
2944+
self.assertEqual((c1.x, c1.y, c1.z, c1.t), (3, 2, 10, 100))
2945+
2946+
2947+
with self.assertRaisesRegex(ValueError, 'init=False'):
2948+
replace(c, x=3, z=20, t=50)
2949+
with self.assertRaisesRegex(ValueError, 'init=False'):
2950+
replace(c, z=20)
2951+
replace(c, x=3, z=20, t=50)
2952+
2953+
# Make sure the result is still frozen.
2954+
with self.assertRaisesRegex(FrozenInstanceError, "cannot assign to field 'x'"):
2955+
c1.x = 3
2956+
2957+
# Make sure we can't replace an attribute that doesn't exist,
2958+
# if we're also replacing one that does exist. Test this
2959+
# here, because setting attributes on frozen instances is
2960+
# handled slightly differently from non-frozen ones.
2961+
with self.assertRaisesRegex(TypeError, r"__init__\(\) got an unexpected "
2962+
"keyword argument 'a'"):
2963+
c1 = replace(c, x=20, a=5)
2964+
2965+
def test_invalid_field_name(self):
2966+
@dataclass(frozen=True)
2967+
class C:
2968+
x: int
2969+
y: int
2970+
2971+
c = C(1, 2)
2972+
with self.assertRaisesRegex(TypeError, r"__init__\(\) got an unexpected "
2973+
"keyword argument 'z'"):
2974+
c1 = replace(c, z=3)
2975+
2976+
def test_invalid_object(self):
2977+
@dataclass(frozen=True)
2978+
class C:
2979+
x: int
2980+
y: int
2981+
2982+
with self.assertRaisesRegex(TypeError, 'dataclass instance'):
2983+
replace(C, x=3)
2984+
2985+
with self.assertRaisesRegex(TypeError, 'dataclass instance'):
2986+
replace(0, x=3)
2987+
2988+
def test_no_init(self):
2989+
@dataclass
2990+
class C:
2991+
x: int
2992+
y: int = field(init=False, default=10)
2993+
2994+
c = C(1)
2995+
c.y = 20
2996+
2997+
# Make sure y gets the default value.
2998+
c1 = replace(c, x=5)
2999+
self.assertEqual((c1.x, c1.y), (5, 10))
3000+
3001+
# Trying to replace y is an error.
3002+
with self.assertRaisesRegex(ValueError, 'init=False'):
3003+
replace(c, x=2, y=30)
3004+
3005+
with self.assertRaisesRegex(ValueError, 'init=False'):
3006+
replace(c, y=30)
3007+
3008+
def test_classvar(self):
3009+
@dataclass
3010+
class C:
3011+
x: int
3012+
y: ClassVar[int] = 1000
3013+
3014+
c = C(1)
3015+
d = C(2)
3016+
3017+
self.assertIs(c.y, d.y)
3018+
self.assertEqual(c.y, 1000)
3019+
3020+
# Trying to replace y is an error: can't replace ClassVars.
3021+
with self.assertRaisesRegex(TypeError, r"__init__\(\) got an "
3022+
"unexpected keyword argument 'y'"):
3023+
replace(c, y=30)
3024+
3025+
replace(c, x=5)
3026+
3027+
## def test_initvar(self):
3028+
## @dataclass
3029+
## class C:
3030+
## x: int
3031+
## y: InitVar[int]
3032+
3033+
## c = C(1, 10)
3034+
## d = C(2, 20)
3035+
3036+
## # In our case, replacing an InitVar is a no-op
3037+
## self.assertEqual(c, replace(c, y=5))
3038+
3039+
## replace(c, x=5)
3040+
30063041

30073042
if __name__ == '__main__':
30083043
unittest.main()

0 commit comments

Comments
 (0)