From f41d83c02a242016afb46d859586cfda3257a102 Mon Sep 17 00:00:00 2001 From: hongweipeng Date: Fri, 16 Jul 2021 14:25:44 +0800 Subject: [PATCH 1/2] Give priority to using the current class constructor in `inspect.signature` --- Lib/inspect.py | 22 ++++++++++----- Lib/test/test_inspect.py | 28 +++++++++++++++++++ .../2021-07-16-13-40-31.bpo-40897.aveAre.rst | 2 ++ 3 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2021-07-16-13-40-31.bpo-40897.aveAre.rst diff --git a/Lib/inspect.py b/Lib/inspect.py index 7a2eefec5a80d4..81dd8b464b18a2 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2444,15 +2444,23 @@ def _signature_from_callable(obj, *, if call is not None: sig = _get_signature_of(call) else: + create_method = None # Now we check if the 'obj' class has a '__new__' method new = _signature_get_user_defined_method(obj, '__new__') - if new is not None: - sig = _get_signature_of(new) - else: - # Finally, we should have at least __init__ implemented - init = _signature_get_user_defined_method(obj, '__init__') - if init is not None: - sig = _get_signature_of(init) + # Finally, we should have at least __init__ implemented + init = _signature_get_user_defined_method(obj, '__init__') + # Give priority to using the current class constructor + if '__new__' in obj.__dict__: + create_method = new + elif '__init__' in obj.__dict__: + create_method = init + elif new is not None: + create_method = new + elif init is not None: + create_method = init + + if create_method is not None: + sig = _get_signature_of(create_method) if sig is None: # At this point we know, that `obj` is a class, with no user- diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 69f17f2477a2f2..f7fe46728d887c 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -3063,6 +3063,34 @@ def __init__(self, b): ('bar', 2, ..., "keyword_only")), ...)) + def test_signature_on_subclass(self): + class A: + def __new__(cls, a=1, *args, **kwargs): + return object.__new__(cls) + class B(A): + def __init__(self, b): + pass + class C(A): + def __new__(cls, a=1, b=2, *args, **kwargs): + return object.__new__(cls) + class D(A): + pass + + self.assertEqual(self.signature(B), + ((('b', ..., ..., "positional_or_keyword"),), + ...)) + self.assertEqual(self.signature(C), + ((('a', 1, ..., 'positional_or_keyword'), + ('b', 2, ..., 'positional_or_keyword'), + ('args', ..., ..., 'var_positional'), + ('kwargs', ..., ..., 'var_keyword')), + ...)) + self.assertEqual(self.signature(D), + ((('a', 1, ..., 'positional_or_keyword'), + ('args', ..., ..., 'var_positional'), + ('kwargs', ..., ..., 'var_keyword')), + ...)) + @unittest.skipIf(MISSING_C_DOCSTRINGS, "Signature information for builtins requires docstrings") def test_signature_on_class_without_init(self): diff --git a/Misc/NEWS.d/next/Library/2021-07-16-13-40-31.bpo-40897.aveAre.rst b/Misc/NEWS.d/next/Library/2021-07-16-13-40-31.bpo-40897.aveAre.rst new file mode 100644 index 00000000000000..04f1465f0ac67f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-07-16-13-40-31.bpo-40897.aveAre.rst @@ -0,0 +1,2 @@ +Give priority to using the current class constructor in +:func:`inspect.signature`. Patch by Weipeng Hong. From ae8278ad01c278461df8e6a2a34fb28a413a3826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Fri, 16 Jul 2021 13:46:19 +0200 Subject: [PATCH 2/2] Add more tests, s/create_method/factory_method, reword comments --- Lib/inspect.py | 20 ++++++++++---------- Lib/test/test_inspect.py | 13 +++++++++++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 81dd8b464b18a2..7e9f7ce11e8753 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2444,23 +2444,23 @@ def _signature_from_callable(obj, *, if call is not None: sig = _get_signature_of(call) else: - create_method = None - # Now we check if the 'obj' class has a '__new__' method + factory_method = None new = _signature_get_user_defined_method(obj, '__new__') - # Finally, we should have at least __init__ implemented init = _signature_get_user_defined_method(obj, '__init__') - # Give priority to using the current class constructor + # Now we check if the 'obj' class has an own '__new__' method if '__new__' in obj.__dict__: - create_method = new + factory_method = new + # or an own '__init__' method elif '__init__' in obj.__dict__: - create_method = init + factory_method = init + # If not, we take inherited '__new__' or '__init__', if present elif new is not None: - create_method = new + factory_method = new elif init is not None: - create_method = init + factory_method = init - if create_method is not None: - sig = _get_signature_of(create_method) + if factory_method is not None: + sig = _get_signature_of(factory_method) if sig is None: # At this point we know, that `obj` is a class, with no user- diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index f7fe46728d887c..cbd574d54400c1 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -3091,6 +3091,19 @@ class D(A): ('kwargs', ..., ..., 'var_keyword')), ...)) + def test_signature_on_generic_subclass(self): + from typing import Generic, TypeVar + + T = TypeVar('T') + + class A(Generic[T]): + def __init__(self, *, a: int) -> None: + pass + + self.assertEqual(self.signature(A), + ((('a', ..., int, 'keyword_only'),), + None)) + @unittest.skipIf(MISSING_C_DOCSTRINGS, "Signature information for builtins requires docstrings") def test_signature_on_class_without_init(self):