Skip to content

Commit

Permalink
Merged changes from bpo-33796 and bpo-33805. (#140)
Browse files Browse the repository at this point in the history
Thanks for the PR. Sorry it took forever to merge. Lots going on!
  • Loading branch information
noamkush committed Apr 4, 2020
1 parent 5eee0b0 commit 75b24ef
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 86 deletions.
9 changes: 8 additions & 1 deletion dataclasses.py
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
223 changes: 138 additions & 85 deletions test/test_dataclasses.py
Expand Up @@ -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
Expand Down Expand Up @@ -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()

0 comments on commit 75b24ef

Please sign in to comment.