summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEric V. Smith <ericvsmith@users.noreply.github.com>2018-06-07 18:43:59 (GMT)
committerGitHub <noreply@github.com>2018-06-07 18:43:59 (GMT)
commite7adf2ba41832404100313f9ac9d9f7fabedc1fd (patch)
treec074a386410628e15dc7ea0b9db06e2cd68b1fbc
parent34b734699b19d826f861b604dd77e82beed95f17 (diff)
downloadcpython-e7adf2ba41832404100313f9ac9d9f7fabedc1fd.zip
cpython-e7adf2ba41832404100313f9ac9d9f7fabedc1fd.tar.gz
cpython-e7adf2ba41832404100313f9ac9d9f7fabedc1fd.tar.bz2
bpo-33796: Ignore ClassVar for dataclasses.replace(). (GH-7488)
-rw-r--r--Lib/dataclasses.py6
-rwxr-xr-xLib/test/test_dataclasses.py205
2 files changed, 125 insertions, 86 deletions
diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py
index 2c5593b..96bf6e1 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 7c39b79..9297931 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()