]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
bpo-46342: make @typing.final introspectable (GH-30530)
authorJelle Zijlstra <jelle.zijlstra@gmail.com>
Wed, 12 Jan 2022 19:38:25 +0000 (11:38 -0800)
committerGitHub <noreply@github.com>
Wed, 12 Jan 2022 19:38:25 +0000 (11:38 -0800)
Co-authored-by: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com>
Doc/library/typing.rst
Lib/test/test_typing.py
Lib/typing.py
Misc/NEWS.d/next/Library/2022-01-11-04-28-09.bpo-46342.5QVEH1.rst [new file with mode: 0644]

index de7aa086a9f82bdcdc83faa08371b51c9d94f1c0..cb14db90711cffe45a5b732035809a1d2ef03952 100644 (file)
@@ -1985,6 +1985,15 @@ Functions and decorators
 
    .. versionadded:: 3.8
 
+   .. versionchanged:: 3.11
+      The decorator will now set the ``__final__`` attribute to ``True``
+      on the decorated object. Thus, a check like
+      ``if getattr(obj, "__final__", False)`` can be used at runtime
+      to determine whether an object ``obj`` has been marked as final.
+      If the decorated object does not support setting attributes,
+      the decorator returns the object unchanged without raising an exception.
+
+
 .. decorator:: no_type_check
 
    Decorator to indicate that annotations are not type hints.
index af5b1df6b04ca8822161c0424859fe8fdb3cab3c..fd8237a1a8c33f9745e1813ce1e67e8e384ae3b6 100644 (file)
@@ -1,5 +1,7 @@
 import contextlib
 import collections
+from functools import lru_cache
+import inspect
 import pickle
 import re
 import sys
@@ -2536,10 +2538,80 @@ class FinalTests(BaseTestCase):
         with self.assertRaises(TypeError):
             issubclass(int, Final)
 
+
+class FinalDecoratorTests(BaseTestCase):
     def test_final_unmodified(self):
         def func(x): ...
         self.assertIs(func, final(func))
 
+    def test_dunder_final(self):
+        @final
+        def func(): ...
+        @final
+        class Cls: ...
+        self.assertIs(True, func.__final__)
+        self.assertIs(True, Cls.__final__)
+
+        class Wrapper:
+            __slots__ = ("func",)
+            def __init__(self, func):
+                self.func = func
+            def __call__(self, *args, **kwargs):
+                return self.func(*args, **kwargs)
+
+        # Check that no error is thrown if the attribute
+        # is not writable.
+        @final
+        @Wrapper
+        def wrapped(): ...
+        self.assertIsInstance(wrapped, Wrapper)
+        self.assertIs(False, hasattr(wrapped, "__final__"))
+
+        class Meta(type):
+            @property
+            def __final__(self): return "can't set me"
+        @final
+        class WithMeta(metaclass=Meta): ...
+        self.assertEqual(WithMeta.__final__, "can't set me")
+
+        # Builtin classes throw TypeError if you try to set an
+        # attribute.
+        final(int)
+        self.assertIs(False, hasattr(int, "__final__"))
+
+        # Make sure it works with common builtin decorators
+        class Methods:
+            @final
+            @classmethod
+            def clsmethod(cls): ...
+
+            @final
+            @staticmethod
+            def stmethod(): ...
+
+            # The other order doesn't work because property objects
+            # don't allow attribute assignment.
+            @property
+            @final
+            def prop(self): ...
+
+            @final
+            @lru_cache()
+            def cached(self): ...
+
+        # Use getattr_static because the descriptor returns the
+        # underlying function, which doesn't have __final__.
+        self.assertIs(
+            True,
+            inspect.getattr_static(Methods, "clsmethod").__final__
+        )
+        self.assertIs(
+            True,
+            inspect.getattr_static(Methods, "stmethod").__final__
+        )
+        self.assertIs(True, Methods.prop.fget.__final__)
+        self.assertIs(True, Methods.cached.__final__)
+
 
 class CastTests(BaseTestCase):
 
index d520f6b2e1b3d4d2cb9ceb1a5e1b4aaa8e11e7c9..972b8ba24b27e83bbd6809ae56777c8aefb6f719 100644 (file)
@@ -2042,8 +2042,17 @@ def final(f):
       class Other(Leaf):  # Error reported by type checker
           ...
 
-    There is no runtime checking of these properties.
+    There is no runtime checking of these properties. The decorator
+    sets the ``__final__`` attribute to ``True`` on the decorated object
+    to allow runtime introspection.
     """
+    try:
+        f.__final__ = True
+    except (AttributeError, TypeError):
+        # Skip the attribute silently if it is not writable.
+        # AttributeError happens if the object has __slots__ or a
+        # read-only property, TypeError if it's a builtin class.
+        pass
     return f
 
 
diff --git a/Misc/NEWS.d/next/Library/2022-01-11-04-28-09.bpo-46342.5QVEH1.rst b/Misc/NEWS.d/next/Library/2022-01-11-04-28-09.bpo-46342.5QVEH1.rst
new file mode 100644 (file)
index 0000000..31d484f
--- /dev/null
@@ -0,0 +1,2 @@
+The ``@typing.final`` decorator now sets the ``__final__`` attribute on the
+decorated object to allow runtime introspection. Patch by Jelle Zijlstra.