[Python-checkins] bpo-46342: make @typing.final introspectable (GH-30530)

gvanrossum webhook-mailer at python.org
Wed Jan 12 14:38:34 EST 2022


https://github.com/python/cpython/commit/0bbf30e2b910bc9c5899134ae9d73a8df968da35
commit: 0bbf30e2b910bc9c5899134ae9d73a8df968da35
branch: main
author: Jelle Zijlstra <jelle.zijlstra at gmail.com>
committer: gvanrossum <gvanrossum at gmail.com>
date: 2022-01-12T11:38:25-08:00
summary:

bpo-46342: make @typing.final introspectable (GH-30530)

Co-authored-by: Ken Jin <28750310+Fidget-Spinner at users.noreply.github.com>

files:
A Misc/NEWS.d/next/Library/2022-01-11-04-28-09.bpo-46342.5QVEH1.rst
M Doc/library/typing.rst
M Lib/test/test_typing.py
M Lib/typing.py

diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst
index de7aa086a9f82..cb14db90711cf 100644
--- a/Doc/library/typing.rst
+++ b/Doc/library/typing.rst
@@ -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.
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index af5b1df6b04ca..fd8237a1a8c33 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -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 @@ def test_no_isinstance(self):
         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):
 
diff --git a/Lib/typing.py b/Lib/typing.py
index d520f6b2e1b3d..972b8ba24b27e 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -2042,8 +2042,17 @@ class Leaf:
       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
index 0000000000000..31d484fc77f1f
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-01-11-04-28-09.bpo-46342.5QVEH1.rst
@@ -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.



More information about the Python-checkins mailing list