From e7adf2ba41832404100313f9ac9d9f7fabedc1fd Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Thu, 7 Jun 2018 14:43:59 -0400 Subject: [PATCH] bpo-33796: Ignore ClassVar for dataclasses.replace(). (GH-7488) --- Lib/dataclasses.py | 6 +- Lib/test/test_dataclasses.py | 205 ++++++++++++++++++++--------------- 2 files changed, 125 insertions(+), 86 deletions(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 2c5593bfc50..96bf6e1df47 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -416,7 +416,7 @@ def _field_init(f, frozen, globals, self_name): # Only test this now, so that we can create variables for the # default. However, return None to signify that we're not going # to actually do the assignment statement for InitVars. - if f._field_type == _FIELD_INITVAR: + if f._field_type is _FIELD_INITVAR: return None # Now, actually generate the field assignment. @@ -1160,6 +1160,10 @@ def replace(obj, **changes): # If a field is not in 'changes', read its value from the provided obj. for f in getattr(obj, _FIELDS).values(): + # Only consider normal fields or InitVars. + if f._field_type is _FIELD_CLASSVAR: + continue + if not f.init: # Error if this field is specified in changes. if f.name in changes: diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index 7c39b79142b..929793119d7 100755 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -1712,91 +1712,6 @@ class TestCase(unittest.TestCase): # Check MRO resolution. self.assertEqual(Child.__mro__, (Child, Parent, Generic, object)) - def test_helper_replace(self): - @dataclass(frozen=True) - class C: - x: int - y: int - - c = C(1, 2) - c1 = replace(c, x=3) - self.assertEqual(c1.x, 3) - self.assertEqual(c1.y, 2) - - def test_helper_replace_frozen(self): - @dataclass(frozen=True) - class C: - x: int - y: int - z: int = field(init=False, default=10) - t: int = field(init=False, default=100) - - c = C(1, 2) - c1 = replace(c, x=3) - self.assertEqual((c.x, c.y, c.z, c.t), (1, 2, 10, 100)) - self.assertEqual((c1.x, c1.y, c1.z, c1.t), (3, 2, 10, 100)) - - - with self.assertRaisesRegex(ValueError, 'init=False'): - replace(c, x=3, z=20, t=50) - with self.assertRaisesRegex(ValueError, 'init=False'): - replace(c, z=20) - replace(c, x=3, z=20, t=50) - - # Make sure the result is still frozen. - with self.assertRaisesRegex(FrozenInstanceError, "cannot assign to field 'x'"): - c1.x = 3 - - # Make sure we can't replace an attribute that doesn't exist, - # if we're also replacing one that does exist. Test this - # here, because setting attributes on frozen instances is - # handled slightly differently from non-frozen ones. - with self.assertRaisesRegex(TypeError, r"__init__\(\) got an unexpected " - "keyword argument 'a'"): - c1 = replace(c, x=20, a=5) - - def test_helper_replace_invalid_field_name(self): - @dataclass(frozen=True) - class C: - x: int - y: int - - c = C(1, 2) - with self.assertRaisesRegex(TypeError, r"__init__\(\) got an unexpected " - "keyword argument 'z'"): - c1 = replace(c, z=3) - - def test_helper_replace_invalid_object(self): - @dataclass(frozen=True) - class C: - x: int - y: int - - with self.assertRaisesRegex(TypeError, 'dataclass instance'): - replace(C, x=3) - - with self.assertRaisesRegex(TypeError, 'dataclass instance'): - replace(0, x=3) - - def test_helper_replace_no_init(self): - @dataclass - class C: - x: int - y: int = field(init=False, default=10) - - c = C(1) - c.y = 20 - - # Make sure y gets the default value. - c1 = replace(c, x=5) - self.assertEqual((c1.x, c1.y), (5, 10)) - - # Trying to replace y is an error. - with self.assertRaisesRegex(ValueError, 'init=False'): - replace(c, x=2, y=30) - with self.assertRaisesRegex(ValueError, 'init=False'): - replace(c, y=30) - def test_dataclassses_pickleable(self): global P, Q, R @dataclass @@ -3003,6 +2918,126 @@ class TestMakeDataclass(unittest.TestCase): C = make_dataclass(classname, ['a', 'b']) self.assertEqual(C.__name__, classname) +class TestReplace(unittest.TestCase): + def test(self): + @dataclass(frozen=True) + class C: + x: int + y: int + + c = C(1, 2) + c1 = replace(c, x=3) + self.assertEqual(c1.x, 3) + self.assertEqual(c1.y, 2) + + def test_frozen(self): + @dataclass(frozen=True) + class C: + x: int + y: int + z: int = field(init=False, default=10) + t: int = field(init=False, default=100) + + c = C(1, 2) + c1 = replace(c, x=3) + self.assertEqual((c.x, c.y, c.z, c.t), (1, 2, 10, 100)) + self.assertEqual((c1.x, c1.y, c1.z, c1.t), (3, 2, 10, 100)) + + + with self.assertRaisesRegex(ValueError, 'init=False'): + replace(c, x=3, z=20, t=50) + with self.assertRaisesRegex(ValueError, 'init=False'): + replace(c, z=20) + replace(c, x=3, z=20, t=50) + + # Make sure the result is still frozen. + with self.assertRaisesRegex(FrozenInstanceError, "cannot assign to field 'x'"): + c1.x = 3 + + # Make sure we can't replace an attribute that doesn't exist, + # if we're also replacing one that does exist. Test this + # here, because setting attributes on frozen instances is + # handled slightly differently from non-frozen ones. + with self.assertRaisesRegex(TypeError, r"__init__\(\) got an unexpected " + "keyword argument 'a'"): + c1 = replace(c, x=20, a=5) + + def test_invalid_field_name(self): + @dataclass(frozen=True) + class C: + x: int + y: int + + c = C(1, 2) + with self.assertRaisesRegex(TypeError, r"__init__\(\) got an unexpected " + "keyword argument 'z'"): + c1 = replace(c, z=3) + + def test_invalid_object(self): + @dataclass(frozen=True) + class C: + x: int + y: int + + with self.assertRaisesRegex(TypeError, 'dataclass instance'): + replace(C, x=3) + + with self.assertRaisesRegex(TypeError, 'dataclass instance'): + replace(0, x=3) + + def test_no_init(self): + @dataclass + class C: + x: int + y: int = field(init=False, default=10) + + c = C(1) + c.y = 20 + + # Make sure y gets the default value. + c1 = replace(c, x=5) + self.assertEqual((c1.x, c1.y), (5, 10)) + + # Trying to replace y is an error. + with self.assertRaisesRegex(ValueError, 'init=False'): + replace(c, x=2, y=30) + + with self.assertRaisesRegex(ValueError, 'init=False'): + replace(c, y=30) + + def test_classvar(self): + @dataclass + class C: + x: int + y: ClassVar[int] = 1000 + + c = C(1) + d = C(2) + + self.assertIs(c.y, d.y) + self.assertEqual(c.y, 1000) + + # Trying to replace y is an error: can't replace ClassVars. + with self.assertRaisesRegex(TypeError, r"__init__\(\) got an " + "unexpected keyword argument 'y'"): + replace(c, y=30) + + replace(c, x=5) + + ## def test_initvar(self): + ## @dataclass + ## class C: + ## x: int + ## y: InitVar[int] + + ## c = C(1, 10) + ## d = C(2, 20) + + ## # In our case, replacing an InitVar is a no-op + ## self.assertEqual(c, replace(c, y=5)) + + ## replace(c, x=5) + if __name__ == '__main__': unittest.main()