[Python-checkins] gh-101561: Add typing.override decorator (#101564)

JelleZijlstra webhook-mailer at python.org
Mon Feb 27 16:16:38 EST 2023


https://github.com/python/cpython/commit/0f89acf6cc4d4790f7b7a82165d0a6e7e84e4b72
commit: 0f89acf6cc4d4790f7b7a82165d0a6e7e84e4b72
branch: main
author: Steven Troxler <steven.troxler at gmail.com>
committer: JelleZijlstra <jelle.zijlstra at gmail.com>
date: 2023-02-27T13:16:11-08:00
summary:

gh-101561: Add typing.override decorator (#101564)

Co-authored-by: Jelle Zijlstra <jelle.zijlstra at gmail.com>
Co-authored-by: Alex Waygood <Alex.Waygood at Gmail.com>

files:
A Misc/NEWS.d/next/Library/2023-02-04-16-35-46.gh-issue-101561.Xo6pIZ.rst
M Doc/library/typing.rst
M Doc/whatsnew/3.12.rst
M Lib/test/test_typing.py
M Lib/typing.py
M Misc/ACKS

diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst
index bbbf6920ddec..3395e4bfb95c 100644
--- a/Doc/library/typing.rst
+++ b/Doc/library/typing.rst
@@ -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.
diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst
index e551c5b4fd06..1a25ec6b7061 100644
--- a/Doc/whatsnew/3.12.rst
+++ b/Doc/whatsnew/3.12.rst
@@ -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
 ---
 
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index 7a460d94469f..d61dc6e2fbd7 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -23,6 +23,7 @@
 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 @@ def cached(self): ...
         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):
diff --git a/Lib/typing.py b/Lib/typing.py
index bdf51bb5f415..8d40e923bb1d 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -138,6 +138,7 @@ def _idfunc(_, x):
     'NoReturn',
     'NotRequired',
     'overload',
+    'override',
     'ParamSpecArgs',
     'ParamSpecKwargs',
     'Required',
@@ -2657,6 +2658,7 @@ class Other(Leaf):  # Error reported by type checker
 # 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 @@ def new_user(user_class: Type[U]) -> U:
     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 decorator(cls_or_fn):
         }
         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
diff --git a/Misc/ACKS b/Misc/ACKS
index 3403aee4cc78..2da3d0ab29b8 100644
--- 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
index 000000000000..2f6a4153062e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-02-04-16-35-46.gh-issue-101561.Xo6pIZ.rst
@@ -0,0 +1 @@
+Add a new decorator :func:`typing.override`. See :pep:`698` for details. Patch by Steven Troxler.



More information about the Python-checkins mailing list