diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32d12981..78610e27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,8 @@ jobs: - "3.11" - "3.11.0" - "3.12" + - "pypy3.7" + - "pypy3.8" - "pypy3.9" - "pypy3.10" diff --git a/CHANGELOG.md b/CHANGELOG.md index 262643ed..428f304b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# Release 4.7.1 (???) + +- Fix `TypedDict`, `NamedTuple` and `is_protocol` tests on PyPy-3.7 and + PyPy-3.8. Patch by Alex Waygood. + # Release 4.7.0 (June 28, 2023) - This is expected to be the last feature release supporting Python 3.7, diff --git a/src/typing_extensions.py b/src/typing_extensions.py index b77c1fdb..901f3b96 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -953,6 +953,21 @@ def __round__(self, ndigits: int = 0) -> T_co: pass +def _ensure_subclassable(mro_entries): + def inner(func): + if sys.implementation.name == "pypy" and sys.version_info < (3, 9): + cls_dict = { + "__call__": staticmethod(func), + "__mro_entries__": staticmethod(mro_entries) + } + t = type(func.__name__, (), cls_dict) + return functools.update_wrapper(t(), func) + else: + func.__mro_entries__ = mro_entries + return func + return inner + + if sys.version_info >= (3, 13): # The standard library TypedDict in Python 3.8 does not store runtime information # about which (if any) keys are optional. See https://bugs.python.org/issue38834 @@ -1059,6 +1074,9 @@ def __subclasscheck__(cls, other): __instancecheck__ = __subclasscheck__ + _TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {}) + + @_ensure_subclassable(lambda bases: (_TypedDict,)) def TypedDict(__typename, __fields=_marker, *, total=True, **kwargs): """A simple typed namespace. At runtime it is equivalent to a plain dict. @@ -1142,9 +1160,6 @@ class Point2D(TypedDict): td.__orig_bases__ = (TypedDict,) return td - _TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {}) - TypedDict.__mro_entries__ = lambda bases: (_TypedDict,) - if hasattr(typing, "_TypedDictMeta"): _TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta) else: @@ -2633,6 +2648,13 @@ def __new__(cls, typename, bases, ns): nm_tpl.__init_subclass__() return nm_tpl + _NamedTuple = type.__new__(_NamedTupleMeta, 'NamedTuple', (), {}) + + def _namedtuple_mro_entries(bases): + assert NamedTuple in bases + return (_NamedTuple,) + + @_ensure_subclassable(_namedtuple_mro_entries) def NamedTuple(__typename, __fields=_marker, **kwargs): """Typed version of namedtuple. @@ -2698,19 +2720,15 @@ class Employee(NamedTuple): nt.__orig_bases__ = (NamedTuple,) return nt - _NamedTuple = type.__new__(_NamedTupleMeta, 'NamedTuple', (), {}) - # On 3.8+, alter the signature so that it matches typing.NamedTuple. # The signature of typing.NamedTuple on >=3.8 is invalid syntax in Python 3.7, # so just leave the signature as it is on 3.7. if sys.version_info >= (3, 8): - NamedTuple.__text_signature__ = '(typename, fields=None, /, **kwargs)' - - def _namedtuple_mro_entries(bases): - assert NamedTuple in bases - return (_NamedTuple,) - - NamedTuple.__mro_entries__ = _namedtuple_mro_entries + _new_signature = '(typename, fields=None, /, **kwargs)' + if isinstance(NamedTuple, _types.FunctionType): + NamedTuple.__text_signature__ = _new_signature + else: + NamedTuple.__call__.__text_signature__ = _new_signature if hasattr(collections.abc, "Buffer"): @@ -2986,7 +3004,8 @@ def is_protocol(__tp: type) -> bool: return ( isinstance(__tp, type) and getattr(__tp, '_is_protocol', False) - and __tp != Protocol + and __tp is not Protocol + and __tp is not getattr(typing, "Protocol", object()) ) def get_protocol_members(__tp: type) -> typing.FrozenSet[str]: