diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index f8c03177350..21408f46645 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -463,7 +463,7 @@ function. modified copy. .. versionchanged:: 3.5 - Signature objects are picklable. + Signature objects are picklable and hashable. .. attribute:: Signature.empty @@ -530,7 +530,7 @@ function. you can use :meth:`Parameter.replace` to create a modified copy. .. versionchanged:: 3.5 - Parameter objects are picklable. + Parameter objects are picklable and hashable. .. attribute:: Parameter.empty diff --git a/Doc/whatsnew/3.5.rst b/Doc/whatsnew/3.5.rst index 76b65a41477..7050e0ad16e 100644 --- a/Doc/whatsnew/3.5.rst +++ b/Doc/whatsnew/3.5.rst @@ -143,7 +143,8 @@ Improved Modules (contributed by Claudiu Popa in :issue:`20627`). * :class:`inspect.Signature` and :class:`inspect.Parameter` are now - picklable (contributed by Yury Selivanov in :issue:`20726`). + picklable and hashable (contributed by Yury Selivanov in :issue:`20726` + and :issue:`20334`). * New class method :meth:`inspect.Signature.from_callable`, which makes subclassing of :class:`~inspect.Signature` easier (contributed diff --git a/Lib/inspect.py b/Lib/inspect.py index eef8dc90ac4..4ac76b1f51f 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2231,6 +2231,16 @@ class Parameter: return '<{} at {:#x} "{}">'.format(self.__class__.__name__, id(self), self) + def __hash__(self): + hash_tuple = (self.name, int(self.kind)) + + if self._annotation is not _empty: + hash_tuple += (self._annotation,) + if self._default is not _empty: + hash_tuple += (self._default,) + + return hash(hash_tuple) + def __eq__(self, other): return (issubclass(other.__class__, Parameter) and self._name == other._name and @@ -2524,6 +2534,12 @@ class Signature: return type(self)(parameters, return_annotation=return_annotation) + def __hash__(self): + hash_tuple = tuple(self.parameters.values()) + if self._return_annotation is not _empty: + hash_tuple += (self._return_annotation,) + return hash(hash_tuple) + def __eq__(self, other): if (not issubclass(type(other), Signature) or self.return_annotation != other.return_annotation or diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 7f1af7a46d9..7ad190b0562 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -2513,11 +2513,29 @@ class TestSignatureObject(unittest.TestCase): def bar(pos, *args, c, b, a=42, **kwargs:int): pass self.assertEqual(inspect.signature(foo), inspect.signature(bar)) - def test_signature_unhashable(self): + def test_signature_hashable(self): + S = inspect.Signature + P = inspect.Parameter + def foo(a): pass - sig = inspect.signature(foo) + foo_sig = inspect.signature(foo) + + manual_sig = S(parameters=[P('a', P.POSITIONAL_OR_KEYWORD)]) + + self.assertEqual(hash(foo_sig), hash(manual_sig)) + self.assertNotEqual(hash(foo_sig), + hash(manual_sig.replace(return_annotation='spam'))) + + def bar(a) -> 1: pass + self.assertNotEqual(hash(foo_sig), hash(inspect.signature(bar))) + + def foo(a={}): pass with self.assertRaisesRegex(TypeError, 'unhashable type'): - hash(sig) + hash(inspect.signature(foo)) + + def foo(a) -> {}: pass + with self.assertRaisesRegex(TypeError, 'unhashable type'): + hash(inspect.signature(foo)) def test_signature_str(self): def foo(a:int=1, *, b, c=None, **kwargs) -> 42: @@ -2651,6 +2669,15 @@ class TestParameterObject(unittest.TestCase): self.assertTrue(repr(p).startswith('