gh-108751: Add copy.replace() function (GH-108752)

It creates a modified copy of an object by calling the object's
__replace__() method.

It is a generalization of dataclasses.replace(), named tuple's _replace()
method and replace() methods in various classes, and supports all these
stdlib classes.
This commit is contained in:
Serhiy Storchaka 2023-09-06 23:55:42 +03:00 committed by GitHub
parent 9f0c0a46f0
commit 6f3c138dfa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 311 additions and 68 deletions

View file

@ -1,6 +1,7 @@
import asyncio
import builtins
import collections
import copy
import datetime
import functools
import importlib
@ -3830,6 +3831,28 @@ class TestSignatureObject(unittest.TestCase):
P('bar', P.VAR_POSITIONAL)])),
'(foo, /, *bar)')
def test_signature_replace_parameters(self):
def test(a, b) -> 42:
pass
sig = inspect.signature(test)
parameters = sig.parameters
sig = sig.replace(parameters=list(parameters.values())[1:])
self.assertEqual(list(sig.parameters), ['b'])
self.assertEqual(sig.parameters['b'], parameters['b'])
self.assertEqual(sig.return_annotation, 42)
sig = sig.replace(parameters=())
self.assertEqual(dict(sig.parameters), {})
sig = inspect.signature(test)
parameters = sig.parameters
sig = copy.replace(sig, parameters=list(parameters.values())[1:])
self.assertEqual(list(sig.parameters), ['b'])
self.assertEqual(sig.parameters['b'], parameters['b'])
self.assertEqual(sig.return_annotation, 42)
sig = copy.replace(sig, parameters=())
self.assertEqual(dict(sig.parameters), {})
def test_signature_replace_anno(self):
def test() -> 42:
pass
@ -3843,6 +3866,15 @@ class TestSignatureObject(unittest.TestCase):
self.assertEqual(sig.return_annotation, 42)
self.assertEqual(sig, inspect.signature(test))
sig = inspect.signature(test)
sig = copy.replace(sig, return_annotation=None)
self.assertIs(sig.return_annotation, None)
sig = copy.replace(sig, return_annotation=sig.empty)
self.assertIs(sig.return_annotation, sig.empty)
sig = copy.replace(sig, return_annotation=42)
self.assertEqual(sig.return_annotation, 42)
self.assertEqual(sig, inspect.signature(test))
def test_signature_replaced(self):
def test():
pass
@ -4187,41 +4219,66 @@ class TestParameterObject(unittest.TestCase):
p = inspect.Parameter('foo', default=42,
kind=inspect.Parameter.KEYWORD_ONLY)
self.assertIsNot(p, p.replace())
self.assertEqual(p, p.replace())
self.assertIsNot(p.replace(), p)
self.assertEqual(p.replace(), p)
self.assertIsNot(copy.replace(p), p)
self.assertEqual(copy.replace(p), p)
p2 = p.replace(annotation=1)
self.assertEqual(p2.annotation, 1)
p2 = p2.replace(annotation=p2.empty)
self.assertEqual(p, p2)
self.assertEqual(p2, p)
p3 = copy.replace(p, annotation=1)
self.assertEqual(p3.annotation, 1)
p3 = copy.replace(p3, annotation=p3.empty)
self.assertEqual(p3, p)
p2 = p2.replace(name='bar')
self.assertEqual(p2.name, 'bar')
self.assertNotEqual(p2, p)
p3 = copy.replace(p3, name='bar')
self.assertEqual(p3.name, 'bar')
self.assertNotEqual(p3, p)
with self.assertRaisesRegex(ValueError,
'name is a required attribute'):
p2 = p2.replace(name=p2.empty)
with self.assertRaisesRegex(ValueError,
'name is a required attribute'):
p3 = copy.replace(p3, name=p3.empty)
p2 = p2.replace(name='foo', default=None)
self.assertIs(p2.default, None)
self.assertNotEqual(p2, p)
p3 = copy.replace(p3, name='foo', default=None)
self.assertIs(p3.default, None)
self.assertNotEqual(p3, p)
p2 = p2.replace(name='foo', default=p2.empty)
self.assertIs(p2.default, p2.empty)
p3 = copy.replace(p3, name='foo', default=p3.empty)
self.assertIs(p3.default, p3.empty)
p2 = p2.replace(default=42, kind=p2.POSITIONAL_OR_KEYWORD)
self.assertEqual(p2.kind, p2.POSITIONAL_OR_KEYWORD)
self.assertNotEqual(p2, p)
p3 = copy.replace(p3, default=42, kind=p3.POSITIONAL_OR_KEYWORD)
self.assertEqual(p3.kind, p3.POSITIONAL_OR_KEYWORD)
self.assertNotEqual(p3, p)
with self.assertRaisesRegex(ValueError,
"value <class 'inspect._empty'> "
"is not a valid Parameter.kind"):
p2 = p2.replace(kind=p2.empty)
with self.assertRaisesRegex(ValueError,
"value <class 'inspect._empty'> "
"is not a valid Parameter.kind"):
p3 = copy.replace(p3, kind=p3.empty)
p2 = p2.replace(kind=p2.KEYWORD_ONLY)
self.assertEqual(p2, p)
p3 = copy.replace(p3, kind=p3.KEYWORD_ONLY)
self.assertEqual(p3, p)
def test_signature_parameter_positional_only(self):
with self.assertRaisesRegex(TypeError, 'name must be a str'):