Restore compatibility with PyPy <3.9 (#262)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 32d1298..78610e2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -53,6 +53,8 @@
           - "3.11"
           - "3.11.0"
           - "3.12"
+          - "pypy3.7"
+          - "pypy3.8"
           - "pypy3.9"
           - "pypy3.10"
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 262643e..428f304 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 b77c1fd..901f3b9 100644
--- a/src/typing_extensions.py
+++ b/src/typing_extensions.py
@@ -953,6 +953,21 @@
             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 @@
 
         __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 @@
         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 @@
                 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 @@
         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 @@
         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]: