]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-101561: Add typing.override decorator (#101564)
authorSteven Troxler <steven.troxler@gmail.com>
Mon, 27 Feb 2023 21:16:11 +0000 (13:16 -0800)
committerGitHub <noreply@github.com>
Mon, 27 Feb 2023 21:16:11 +0000 (13:16 -0800)
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Doc/library/typing.rst
Doc/whatsnew/3.12.rst
Lib/test/test_typing.py
Lib/typing.py
Misc/ACKS
Misc/NEWS.d/next/Library/2023-02-04-16-35-46.gh-issue-101561.Xo6pIZ.rst [new file with mode: 0644]

index bbbf6920ddec881b177ffc76c10127fd45be9fd6..3395e4bfb95c444b9ee7c2d05d385db19eed88c1 100644 (file)
@@ -91,6 +91,8 @@ annotations. These include:
     *Introducing* :data:`LiteralString`
 * :pep:`681`: Data Class Transforms
     *Introducing* the :func:`@dataclass_transform<dataclass_transform>` decorator
+* :pep:`698`: Adding an override decorator to typing
+    *Introducing* the :func:`@override<override>` decorator
 
 .. _type-aliases:
 
@@ -2722,6 +2724,42 @@ Functions and decorators
    This wraps the decorator with something that wraps the decorated
    function in :func:`no_type_check`.
 
+
+.. decorator:: override
+
+   A decorator for methods that indicates to type checkers that this method
+   should override a method or attribute with the same name on a base class.
+   This helps prevent bugs that may occur when a base class is changed without
+   an equivalent change to a child class.
+
+   For example::
+
+      class Base:
+           def log_status(self)
+
+      class Sub(Base):
+          @override
+          def log_status(self) -> None:  # Okay: overrides Base.log_status
+              ...
+
+          @override
+          def done(self) -> None:  # Error reported by type checker
+              ...
+
+   There is no runtime checking of this property.
+
+   The decorator will set the ``__override__`` attribute to ``True`` on
+   the decorated object. Thus, a check like
+   ``if getattr(obj, "__override__", False)`` can be used at runtime to determine
+   whether an object ``obj`` has been marked as an override.  If the decorated object
+   does not support setting attributes, the decorator returns the object unchanged
+   without raising an exception.
+
+   See :pep:`698` for more details.
+
+   .. versionadded:: 3.12
+
+
 .. decorator:: type_check_only
 
    Decorator to mark a class or function to be unavailable at runtime.
index e551c5b4fd06a9d9c6cd7eaeb56cd4cdc5793500..1a25ec6b70613b4dbb55fefcb39aedf8b8fe7872 100644 (file)
@@ -350,6 +350,14 @@ tempfile
 The :class:`tempfile.NamedTemporaryFile` function has a new optional parameter
 *delete_on_close* (Contributed by Evgeny Zorin in :gh:`58451`.)
 
+typing
+------
+
+* Add :func:`typing.override`, an override decorator telling to static type
+  checkers to verify that a method overrides some method or attribute of the
+  same name on a base class, as per :pep:`698`. (Contributed by Steven Troxler in
+  :gh:`101564`.)
+
 sys
 ---
 
index 7a460d94469fe7bf08600af9b9ca81fae11b9cca..d61dc6e2fbd70b0da792cc48ef3b5547e41ef749 100644 (file)
@@ -23,6 +23,7 @@ from typing import Generic, ClassVar, Final, final, Protocol
 from typing import assert_type, cast, runtime_checkable
 from typing import get_type_hints
 from typing import get_origin, get_args
+from typing import override
 from typing import is_typeddict
 from typing import reveal_type
 from typing import dataclass_transform
@@ -4166,6 +4167,43 @@ class FinalDecoratorTests(BaseTestCase):
         self.assertIs(True, Methods.cached.__final__)
 
 
+class OverrideDecoratorTests(BaseTestCase):
+    def test_override(self):
+        class Base:
+            def normal_method(self): ...
+            @staticmethod
+            def static_method_good_order(): ...
+            @staticmethod
+            def static_method_bad_order(): ...
+            @staticmethod
+            def decorator_with_slots(): ...
+
+        class Derived(Base):
+            @override
+            def normal_method(self):
+                return 42
+
+            @staticmethod
+            @override
+            def static_method_good_order():
+                return 42
+
+            @override
+            @staticmethod
+            def static_method_bad_order():
+                return 42
+
+
+        self.assertIsSubclass(Derived, Base)
+        instance = Derived()
+        self.assertEqual(instance.normal_method(), 42)
+        self.assertIs(True, instance.normal_method.__override__)
+        self.assertEqual(Derived.static_method_good_order(), 42)
+        self.assertIs(True, Derived.static_method_good_order.__override__)
+        self.assertEqual(Derived.static_method_bad_order(), 42)
+        self.assertIs(False, hasattr(Derived.static_method_bad_order, "__override__"))
+
+
 class CastTests(BaseTestCase):
 
     def test_basics(self):
index bdf51bb5f415955b03d924ee4d5178ccfb2f8857..8d40e923bb1d0869d49ee1f8364424c14679a6a9 100644 (file)
@@ -138,6 +138,7 @@ __all__ = [
     'NoReturn',
     'NotRequired',
     'overload',
+    'override',
     'ParamSpecArgs',
     'ParamSpecKwargs',
     'Required',
@@ -2657,6 +2658,7 @@ T_contra = TypeVar('T_contra', contravariant=True)  # Ditto contravariant.
 # Internal type variable used for Type[].
 CT_co = TypeVar('CT_co', covariant=True, bound=type)
 
+
 # A useful type variable with constraints.  This represents string types.
 # (This one *is* for export!)
 AnyStr = TypeVar('AnyStr', bytes, str)
@@ -2748,6 +2750,8 @@ Type.__doc__ = \
     At this point the type checker knows that joe has type BasicUser.
     """
 
+# Internal type variable for callables. Not for export.
+F = TypeVar("F", bound=Callable[..., Any])
 
 @runtime_checkable
 class SupportsInt(Protocol):
@@ -3448,3 +3452,40 @@ def dataclass_transform(
         }
         return cls_or_fn
     return decorator
+
+
+
+def override(method: F, /) -> F:
+    """Indicate that a method is intended to override a method in a base class.
+
+    Usage:
+
+        class Base:
+            def method(self) -> None: ...
+                pass
+
+        class Child(Base):
+            @override
+            def method(self) -> None:
+                super().method()
+
+    When this decorator is applied to a method, the type checker will
+    validate that it overrides a method or attribute with the same name on a
+    base class.  This helps prevent bugs that may occur when a base class is
+    changed without an equivalent change to a child class.
+
+    There is no runtime checking of this property. The decorator sets the
+    ``__override__`` attribute to ``True`` on the decorated object to allow
+    runtime introspection.
+
+    See PEP 698 for details.
+
+    """
+    try:
+        method.__override__ = 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 method
index 3403aee4cc78ff402ea1b14c84a6586d0dfc1dab..2da3d0ab29b81d2ae332f20e5238b4734aa4ba95 100644 (file)
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1848,6 +1848,7 @@ Tom Tromey
 John Tromp
 Diane Trout
 Jason Trowbridge
+Steven Troxler
 Brent Tubbs
 Anthony Tuininga
 Erno Tukia
diff --git a/Misc/NEWS.d/next/Library/2023-02-04-16-35-46.gh-issue-101561.Xo6pIZ.rst b/Misc/NEWS.d/next/Library/2023-02-04-16-35-46.gh-issue-101561.Xo6pIZ.rst
new file mode 100644 (file)
index 0000000..2f6a415
--- /dev/null
@@ -0,0 +1 @@
+Add a new decorator :func:`typing.override`. See :pep:`698` for details. Patch by Steven Troxler.\r