bpo-33796: Ignore ClassVar for dataclasses.replace(). (GH-7488)
This commit is contained in:
parent
34b734699b
commit
e7adf2ba41
|
@ -416,7 +416,7 @@ def _field_init(f, frozen, globals, self_name):
|
||||||
# Only test this now, so that we can create variables for the
|
# Only test this now, so that we can create variables for the
|
||||||
# default. However, return None to signify that we're not going
|
# default. However, return None to signify that we're not going
|
||||||
# to actually do the assignment statement for InitVars.
|
# to actually do the assignment statement for InitVars.
|
||||||
if f._field_type == _FIELD_INITVAR:
|
if f._field_type is _FIELD_INITVAR:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Now, actually generate the field assignment.
|
# 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.
|
# If a field is not in 'changes', read its value from the provided obj.
|
||||||
|
|
||||||
for f in getattr(obj, _FIELDS).values():
|
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:
|
if not f.init:
|
||||||
# Error if this field is specified in changes.
|
# Error if this field is specified in changes.
|
||||||
if f.name in changes:
|
if f.name in changes:
|
||||||
|
|
|
@ -1712,91 +1712,6 @@ class TestCase(unittest.TestCase):
|
||||||
# Check MRO resolution.
|
# Check MRO resolution.
|
||||||
self.assertEqual(Child.__mro__, (Child, Parent, Generic, object))
|
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):
|
def test_dataclassses_pickleable(self):
|
||||||
global P, Q, R
|
global P, Q, R
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -3003,6 +2918,126 @@ class TestMakeDataclass(unittest.TestCase):
|
||||||
C = make_dataclass(classname, ['a', 'b'])
|
C = make_dataclass(classname, ['a', 'b'])
|
||||||
self.assertEqual(C.__name__, classname)
|
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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
Loading…
Reference in New Issue