Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore compatibility with PyPy <3.9 #262

Merged
merged 2 commits into from
Jul 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ jobs:
- "3.11"
- "3.11.0"
- "3.12"
- "pypy3.7"
- "pypy3.8"
- "pypy3.9"
- "pypy3.10"

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
45 changes: 32 additions & 13 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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"):
Expand Down Expand Up @@ -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())
Comment on lines +3007 to +3008
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_protocol(typing.Protocol) was evaluating to True on PyPy-3.8. I don't know why that is, really, but this change fixes it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I used != on purpose here so that Protocol can have a custom __eq__ that helps us out. What you're doing here should work too, though.

Copy link
Member Author

@AlexWaygood AlexWaygood Jul 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I have no idea why _ProtocolMeta.__eq__ doesn't seem to be doing what it should be doing on PyPy 3.8. I suspect another bug in PyPy. It's easy enough for us to work around it here, though :)

)

def get_protocol_members(__tp: type) -> typing.FrozenSet[str]:
Expand Down