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
|
||||
# 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:
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue