aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJelle Zijlstra <jelle.zijlstra@gmail.com>2022-01-15 15:35:49 -0800
committerGitHub <noreply@github.com>2022-01-15 15:35:49 -0800
commit86fab7591e74063f9dd7700ed446bd77fd276849 (patch)
tree69dac49da555bc8e3742b7149f2e12c88950d0ef
parent465953f8d4afced0b9a6d7aab84415b3478ee76a (diff)
downloadtyping-86fab7591e74063f9dd7700ed446bd77fd276849.tar.gz
@final: backport bpo-46342 (#1026)
-rw-r--r--.github/workflows/ci.yml1
-rw-r--r--typing_extensions/CHANGELOG3
-rw-r--r--typing_extensions/README.rst12
-rw-r--r--typing_extensions/src/test_typing_extensions.py83
-rw-r--r--typing_extensions/src/typing_extensions.py18
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