From e0fd7f73c2fa0c63db49e18a245ff00b5c19d516 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 3 Oct 2023 19:12:22 +0300 Subject: [PATCH 1/3] gh-110275: Named tuple's __replace__() now raise TypeError for invalid arguments --- Lib/collections/__init__.py | 11 ++++++++++- Lib/test/test_copy.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index a461550ea40da7..55de8e1b510c6d 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -463,6 +463,14 @@ def _replace(self, /, **kwds): _replace.__doc__ = (f'Return a new {typename} object replacing specified ' 'fields with new values') + def __replace__(self, /, **kwds): + result = self._make(_map(kwds.pop, field_names, self)) + for name in kwds: + raise TypeError(f'Got unexpected keyword argument {name!r}') + return result + + __replace__.__doc__ = _replace.__doc__ + def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self @@ -480,6 +488,7 @@ def __getnewargs__(self): __new__, _make.__func__, _replace, + __replace__, __repr__, _asdict, __getnewargs__, @@ -495,7 +504,7 @@ def __getnewargs__(self): '_field_defaults': field_defaults, '__new__': __new__, '_make': _make, - '__replace__': _replace, + '__replace__': __replace__, '_replace': _replace, '__repr__': __repr__, '_asdict': _asdict, diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py index 60735ba89a80ee..429ee3f4977daa 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -952,7 +952,7 @@ class PointFromClass(NamedTuple): self.assertEqual(copy.replace(p, x=1), (1, 22)) self.assertEqual(copy.replace(p, y=2), (11, 2)) self.assertEqual(copy.replace(p, x=1, y=2), (1, 2)) - with self.assertRaisesRegex(ValueError, 'unexpected field name'): + with self.assertRaisesRegex(TypeError, 'unexpected keyword argument'): copy.replace(p, x=1, error=2) def test_dataclass(self): From 1dead40c501a46f882398d133d2fe293f7129379 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 4 Oct 2023 09:51:47 +0300 Subject: [PATCH 2/3] Polishing. --- Lib/collections/__init__.py | 9 +++++---- Lib/test/test_copy.py | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 55de8e1b510c6d..654a2fa462f653 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -463,10 +463,11 @@ def _replace(self, /, **kwds): _replace.__doc__ = (f'Return a new {typename} object replacing specified ' 'fields with new values') - def __replace__(self, /, **kwds): - result = self._make(_map(kwds.pop, field_names, self)) - for name in kwds: - raise TypeError(f'Got unexpected keyword argument {name!r}') + def __replace__(self, /, **changes): + result = self._make(_map(changes.pop, field_names, self)) + if changes: + for name in changes: + raise TypeError(f'Got unexpected keyword argument {name!r}') return result __replace__.__doc__ = _replace.__doc__ diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py index 429ee3f4977daa..b5d2d0ad7d8bae 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -952,7 +952,8 @@ class PointFromClass(NamedTuple): self.assertEqual(copy.replace(p, x=1), (1, 22)) self.assertEqual(copy.replace(p, y=2), (11, 2)) self.assertEqual(copy.replace(p, x=1, y=2), (1, 2)) - with self.assertRaisesRegex(TypeError, 'unexpected keyword argument'): + with self.assertRaisesRegex(TypeError, + "unexpected keyword argument 'error'"): copy.replace(p, x=1, error=2) def test_dataclass(self): From 9b98e750977600f5668bb030555c40badaca84a8 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 8 Nov 2023 17:25:43 +0200 Subject: [PATCH 3/3] Minimize changes. --- Doc/library/collections.rst | 4 ++++ Lib/collections/__init__.py | 14 ++------------ Lib/test/test_collections.py | 6 +----- Lib/test/test_copy.py | 3 +-- .../2023-11-08-16-11-04.gh-issue-110275.Bm6GwR.rst | 2 ++ 5 files changed, 10 insertions(+), 19 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-11-08-16-11-04.gh-issue-110275.Bm6GwR.rst diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst index 17dd6da7479e50..233b2c6a771f4a 100644 --- a/Doc/library/collections.rst +++ b/Doc/library/collections.rst @@ -981,6 +981,10 @@ field names, the method and attribute names start with an underscore. Named tuples are also supported by generic function :func:`copy.replace`. + .. versionchanged:: 3.13 + Raise :exc:`TypeError` instead of :exc:`ValueError` for invalid + keyword arguments. + .. attribute:: somenamedtuple._fields Tuple of strings listing the field names. Useful for introspection diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 654a2fa462f653..2e527dfd810c43 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -457,21 +457,12 @@ def _make(cls, iterable): def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: - raise ValueError(f'Got unexpected field names: {list(kwds)!r}') + raise TypeError(f'Got unexpected field names: {list(kwds)!r}') return result _replace.__doc__ = (f'Return a new {typename} object replacing specified ' 'fields with new values') - def __replace__(self, /, **changes): - result = self._make(_map(changes.pop, field_names, self)) - if changes: - for name in changes: - raise TypeError(f'Got unexpected keyword argument {name!r}') - return result - - __replace__.__doc__ = _replace.__doc__ - def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self @@ -489,7 +480,6 @@ def __getnewargs__(self): __new__, _make.__func__, _replace, - __replace__, __repr__, _asdict, __getnewargs__, @@ -505,7 +495,7 @@ def __getnewargs__(self): '_field_defaults': field_defaults, '__new__': __new__, '_make': _make, - '__replace__': __replace__, + '__replace__': _replace, '_replace': _replace, '__repr__': __repr__, '_asdict': _asdict, diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index bb8b352518ef3e..7e6f811e17cfa2 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -488,12 +488,8 @@ def test_instance(self): self.assertEqual(p._replace(x=1), (1, 22)) # test _replace method self.assertEqual(p._asdict(), dict(x=11, y=22)) # test _asdict method - try: + with self.assertRaises(TypeError): p._replace(x=1, error=2) - except ValueError: - pass - else: - self._fail('Did not detect an incorrect fieldname') # verify that field string can have commas Point = namedtuple('Point', 'x, y') diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py index b5d2d0ad7d8bae..89102373759ca0 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -952,8 +952,7 @@ class PointFromClass(NamedTuple): self.assertEqual(copy.replace(p, x=1), (1, 22)) self.assertEqual(copy.replace(p, y=2), (11, 2)) self.assertEqual(copy.replace(p, x=1, y=2), (1, 2)) - with self.assertRaisesRegex(TypeError, - "unexpected keyword argument 'error'"): + with self.assertRaisesRegex(TypeError, 'unexpected field name'): copy.replace(p, x=1, error=2) def test_dataclass(self): diff --git a/Misc/NEWS.d/next/Library/2023-11-08-16-11-04.gh-issue-110275.Bm6GwR.rst b/Misc/NEWS.d/next/Library/2023-11-08-16-11-04.gh-issue-110275.Bm6GwR.rst new file mode 100644 index 00000000000000..194dd5cb623f0f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-11-08-16-11-04.gh-issue-110275.Bm6GwR.rst @@ -0,0 +1,2 @@ +Named tuple's methods ``_replace()`` and ``__replace__()`` now raise +TypeError instead of ValueError for invalid keyword arguments.