From ac28ed63ab6acde80379311a9460c4995d2a18e9 Mon Sep 17 00:00:00 2001 From: Noam Date: Thu, 13 Dec 2018 17:34:16 +0200 Subject: [PATCH] Merged changes from bpo-33796 and bpo-33805. --- dataclasses.py | 9 +- test/test_dataclasses.py | 223 ++++++++++++++++++++++++--------------- 2 files changed, 146 insertions(+), 86 deletions(-) diff --git a/dataclasses.py b/dataclasses.py index ba34f6b..ddd59c6 100644 --- a/dataclasses.py +++ b/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. @@ -1158,6 +1158,10 @@ class C: # 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: @@ -1167,6 +1171,9 @@ class C: continue if f.name not in changes: + if f._field_type is _FIELD_INITVAR: + raise ValueError(f"InitVar {f.name!r} " + 'must be specified with replace()') changes[f.name] = getattr(obj, f.name) # Create the new object, which calls __init__() and diff --git a/test/test_dataclasses.py b/test/test_dataclasses.py index 7324060..741d0d6 100644 --- a/test/test_dataclasses.py +++ b/test/test_dataclasses.py @@ -1712,91 +1712,6 @@ class Parent(Generic[T]): # 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 @@ -3004,5 +2919,143 @@ def test_funny_class_names_names(self): 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_is_specified(self): + @dataclass + class C: + x: int + y: InitVar[int] + + def __post_init__(self, y): + self.x *= y + + c = C(1, 10) + self.assertEqual(c.x, 10) + with self.assertRaisesRegex(ValueError, r"InitVar 'y' must be " + "specified with replace()"): + replace(c, x=3) + c = replace(c, x=3, y=5) + self.assertEqual(c.x, 15) + + ## 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()