diff options
author | Jelle Zijlstra <jelle.zijlstra@gmail.com> | 2022-01-15 15:35:49 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-01-15 15:35:49 -0800 |
commit | 86fab7591e74063f9dd7700ed446bd77fd276849 (patch) | |
tree | 69dac49da555bc8e3742b7149f2e12c88950d0ef | |
parent | 465953f8d4afced0b9a6d7aab84415b3478ee76a (diff) | |
download | typing-86fab7591e74063f9dd7700ed446bd77fd276849.tar.gz |
@final: backport bpo-46342 (#1026)
-rw-r--r-- | .github/workflows/ci.yml | 1 | ||||
-rw-r--r-- | typing_extensions/CHANGELOG | 3 | ||||
-rw-r--r-- | typing_extensions/README.rst | 12 | ||||
-rw-r--r-- | typing_extensions/src/test_typing_extensions.py | 83 | ||||
-rw-r--r-- | typing_extensions/src/typing_extensions.py | 18 |
5 files changed, 111 insertions, 6 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbfb82e..5a89af4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Test typing_extensions + continue-on-error: ${{ matrix.python-version == '3.11-dev' }} run: | # Be wary of running `pip install` here, since it becomes easy for us to # accidentally pick up typing_extensions as installed by a dependency diff --git a/typing_extensions/CHANGELOG b/typing_extensions/CHANGELOG index bc1c2c8..e852291 100644 --- a/typing_extensions/CHANGELOG +++ b/typing_extensions/CHANGELOG @@ -1,5 +1,8 @@ # Release 4.x.x +- The `@final` decorator now sets the `__final__` attribute on the + decorated object to allow runtime introspection. Backport from + bpo-46342. - Add `is_typeddict`. Patch by Chris Moradi (@chrismoradi) and James Hilton-Balfe (@Gobot1234). diff --git a/typing_extensions/README.rst b/typing_extensions/README.rst index 270cff4..d5d4128 100644 --- a/typing_extensions/README.rst +++ b/typing_extensions/README.rst @@ -96,6 +96,18 @@ This module currently contains the following: Other Notes and Limitations =========================== +Certain objects were changed after they were added to ``typing``, and +``typing_extensions`` provides a backport even on newer Python versions: + +- ``TypedDict`` does not store runtime information + about which (if any) keys are non-required in Python 3.8, and does not + honor the "total" keyword with old-style ``TypedDict()`` in Python + 3.9.0 and 3.9.1. +- ``get_origin`` and ``get_args`` lack support for ``Annotated`` in + Python 3.8 and lack support for ``ParamSpecArgs`` and ``ParamSpecKwargs`` + in 3.9. +- ``@final`` was changed in Python 3.11 to set the ``.__final__`` attribute. + There are a few types whose interface was modified between different versions of typing. For example, ``typing.Sequence`` was modified to subclass ``typing.Reversible`` as of Python 3.5.3. diff --git a/typing_extensions/src/test_typing_extensions.py b/typing_extensions/src/test_typing_extensions.py index 186aa7a..d740976 100644 --- a/typing_extensions/src/test_typing_extensions.py +++ b/typing_extensions/src/test_typing_extensions.py @@ -4,6 +4,8 @@ import abc import contextlib import collections import collections.abc +from functools import lru_cache +import inspect import pickle import subprocess import types @@ -19,7 +21,7 @@ import typing_extensions from typing_extensions import NoReturn, ClassVar, Final, IntVar, Literal, Type, NewType, TypedDict, Self from typing_extensions import TypeAlias, ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs, TypeGuard from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired -from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, overload, is_typeddict +from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, overload, final, is_typeddict try: from typing_extensions import get_type_hints except ImportError: @@ -2186,7 +2188,6 @@ class TypeGuardTests(BaseTestCase): issubclass(int, TypeGuard) - class SelfTests(BaseTestCase): def test_basics(self): class Foo: @@ -2228,6 +2229,81 @@ class SelfTests(BaseTestCase): def return_tuple(self) -> TupleSelf: return (self, self) + +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 AllTests(BaseTestCase): def test_typing_extensions_includes_standard(self): @@ -2277,6 +2353,8 @@ class AllTests(BaseTestCase): } if sys.version_info < (3, 10): exclude |= {'get_args', 'get_origin'} + if sys.version_info < (3, 11): + exclude.add('final') for item in typing_extensions.__all__: if item not in exclude and hasattr(typing, item): self.assertIs( @@ -2294,5 +2372,6 @@ class AllTests(BaseTestCase): self.fail('Module does not compile with optimize=2 (-OO flag).') + if __name__ == '__main__': main() diff --git a/typing_extensions/src/typing_extensions.py b/typing_extensions/src/typing_extensions.py index b5a4654..7a03f1f 100644 --- a/typing_extensions/src/typing_extensions.py +++ b/typing_extensions/src/typing_extensions.py @@ -212,11 +212,12 @@ else: Final = _Final(_root=True) -# 3.8+ -if hasattr(typing, 'final'): +if sys.version_info >= (3, 11): final = typing.final -# 3.6-3.7 else: + # @final exists in 3.8+, but we backport it for all versions + # before 3.11 to keep support for the __final__ attribute. + # See https://bugs.python.org/issue46342 def final(f): """This decorator can be used to indicate to type checkers that the decorated method cannot be overridden, and decorated class @@ -235,8 +236,17 @@ else: 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 |