Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge changes from bpo-33796 and bpo-33805 #140

Merged
merged 1 commit into from Apr 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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()