From a5a7cea202d34ca699d9594d428ba527ef7ff2ce Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Fri, 16 Dec 2022 12:30:47 -0800 Subject: [PATCH] gh-100039: enhance __signature__ to work with str and callables (GH-100168) Callables should be either class- or static-methods. Enum now uses the classmethod version to greatly improve the help given for enums and flags. --- Lib/enum.py | 9 +++++- Lib/inspect.py | 10 +++++- Lib/test/test_enum.py | 25 +++++++++++++-- Lib/test/test_inspect.py | 32 +++++++++++++++++++ ...-12-10-20-52-28.gh-issue-100039.zDqjT4.rst | 1 + 5 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-12-10-20-52-28.gh-issue-100039.zDqjT4.rst diff --git a/Lib/enum.py b/Lib/enum.py index a0cad066dc23f7..21f63881d78d4d 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -685,7 +685,7 @@ def __new__(metacls, cls, bases, classdict, *, boundary=None, _simple=False, **k 'member order does not match _order_:\n %r\n %r' % (enum_class._member_names_, _order_) ) - + # return enum_class def __bool__(cls): @@ -1083,6 +1083,13 @@ class Enum(metaclass=EnumType): attributes -- see the documentation for details. """ + @classmethod + def __signature__(cls): + if cls._member_names_: + return '(*values)' + else: + return '(new_class_name, /, names, *, module=None, qualname=None, type=None, start=1, boundary=None)' + def __new__(cls, value): # all enum instances are actually created during class construction # without calling this method; this method is called by the metaclass' diff --git a/Lib/inspect.py b/Lib/inspect.py index e165937e448a95..e92c355220fd8d 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2443,10 +2443,18 @@ def _signature_from_callable(obj, *, pass else: if sig is not None: + # since __text_signature__ is not writable on classes, __signature__ + # may contain text (or be a callable that returns text); + # if so, convert it + o_sig = sig + if not isinstance(sig, (Signature, str)) and callable(sig): + sig = sig() + if isinstance(sig, str): + sig = _signature_fromstr(sigcls, obj, sig) if not isinstance(sig, Signature): raise TypeError( 'unexpected object {!r} in __signature__ ' - 'attribute'.format(sig)) + 'attribute'.format(o_sig)) return sig try: diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index d876c8e5fb7798..1e653e94f6b57a 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -4259,7 +4259,7 @@ class TestEnumTypeSubclassing(unittest.TestCase): Help on class Color in module %s: class Color(enum.Enum) - | Color(value, names=None, *values, module=None, qualname=None, type=None, start=1, boundary=None) + | Color(*values) | | Method resolution order: | Color @@ -4315,7 +4315,7 @@ class Color(enum.Enum) Help on class Color in module %s: class Color(enum.Enum) - | Color(value, names=None, *values, module=None, qualname=None, type=None, start=1) + | Color(*values) | | Method resolution order: | Color @@ -4462,6 +4462,27 @@ def test_inspect_classify_class_attrs(self): if failed: self.fail("result does not equal expected, see print above") + def test_inspect_signatures(self): + from inspect import signature, Signature, Parameter + self.assertEqual( + signature(Enum), + Signature([ + Parameter('new_class_name', Parameter.POSITIONAL_ONLY), + Parameter('names', Parameter.POSITIONAL_OR_KEYWORD), + Parameter('module', Parameter.KEYWORD_ONLY, default=None), + Parameter('qualname', Parameter.KEYWORD_ONLY, default=None), + Parameter('type', Parameter.KEYWORD_ONLY, default=None), + Parameter('start', Parameter.KEYWORD_ONLY, default=1), + Parameter('boundary', Parameter.KEYWORD_ONLY, default=None), + ]), + ) + self.assertEqual( + signature(enum.FlagBoundary), + Signature([ + Parameter('values', Parameter.VAR_POSITIONAL), + ]), + ) + def test_test_simple_enum(self): @_simple_enum(Enum) class SimpleColor: diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 2b7977b1648f70..0f8217ba3b7550 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -3614,6 +3614,38 @@ def foo(): pass self.assertEqual(signature_func(foo), inspect.Signature()) self.assertEqual(inspect.get_annotations(foo), {}) + def test_signature_as_str(self): + self.maxDiff = None + class S: + __signature__ = '(a, b=2)' + + self.assertEqual(self.signature(S), + ((('a', ..., ..., 'positional_or_keyword'), + ('b', 2, ..., 'positional_or_keyword')), + ...)) + + def test_signature_as_callable(self): + # __signature__ should be either a staticmethod or a bound classmethod + class S: + @classmethod + def __signature__(cls): + return '(a, b=2)' + + self.assertEqual(self.signature(S), + ((('a', ..., ..., 'positional_or_keyword'), + ('b', 2, ..., 'positional_or_keyword')), + ...)) + + class S: + @staticmethod + def __signature__(): + return '(a, b=2)' + + self.assertEqual(self.signature(S), + ((('a', ..., ..., 'positional_or_keyword'), + ('b', 2, ..., 'positional_or_keyword')), + ...)) + class TestParameterObject(unittest.TestCase): def test_signature_parameter_kinds(self): diff --git a/Misc/NEWS.d/next/Library/2022-12-10-20-52-28.gh-issue-100039.zDqjT4.rst b/Misc/NEWS.d/next/Library/2022-12-10-20-52-28.gh-issue-100039.zDqjT4.rst new file mode 100644 index 00000000000000..d56643f7e87192 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-12-10-20-52-28.gh-issue-100039.zDqjT4.rst @@ -0,0 +1 @@ +Improve signatures for enums and flags.