Backport `NewType` as it exists on py310+ (#157)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f117f39..bc5abe7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -70,6 +70,9 @@
- Constructing a call-based `TypedDict` using keyword arguments for the fields
now causes a `DeprecationWarning` to be emitted. This matches the behaviour
of `typing.TypedDict` on 3.11 and 3.12.
+- Backport the implementation of `NewType` from 3.10 (where it is implemented
+ as a class rather than a function). This allows user-defined `NewType`s to be
+ pickled. Patch by Alex Waygood.
# Release 4.5.0 (February 14, 2023)
diff --git a/README.md b/README.md
index b7e6a7a..11434d1 100644
--- a/README.md
+++ b/README.md
@@ -182,6 +182,9 @@
caching bug was fixed in 3.10.1/3.9.8. The `typing_extensions` version
flattens and deduplicates parameters on all Python versions, and the caching
bug is also fixed on all versions.
+- `NewType` has been in the `typing` module since Python 3.5.2, but
+ user-defined `NewType`s are only pickleable on Python 3.10+.
+ `typing_extensions.NewType` backports this feature to all Python versions.
There are a few types whose interface was modified between different
versions of typing. For example, `typing.Sequence` was modified to
diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py
index 4a5d3a1..469c31b 100644
--- a/src/test_typing_extensions.py
+++ b/src/test_typing_extensions.py
@@ -11,6 +11,7 @@
import importlib
import inspect
import pickle
+import re
import subprocess
import tempfile
import types
@@ -1539,23 +1540,90 @@
class NewTypeTests(BaseTestCase):
+ @classmethod
+ def setUpClass(cls):
+ global UserId
+ UserId = NewType('UserId', int)
+ cls.UserName = NewType(cls.__qualname__ + '.UserName', str)
+
+ @classmethod
+ def tearDownClass(cls):
+ global UserId
+ del UserId
+ del cls.UserName
def test_basic(self):
- UserId = NewType('UserId', int)
- UserName = NewType('UserName', str)
self.assertIsInstance(UserId(5), int)
- self.assertIsInstance(UserName('Joe'), str)
+ self.assertIsInstance(self.UserName('Joe'), str)
self.assertEqual(UserId(5) + 1, 6)
def test_errors(self):
- UserId = NewType('UserId', int)
- UserName = NewType('UserName', str)
with self.assertRaises(TypeError):
issubclass(UserId, int)
with self.assertRaises(TypeError):
- class D(UserName):
+ class D(UserId):
pass
+ @skipUnless(TYPING_3_10_0, "PEP 604 has yet to be")
+ def test_or(self):
+ for cls in (int, self.UserName):
+ with self.subTest(cls=cls):
+ self.assertEqual(UserId | cls, Union[UserId, cls])
+ self.assertEqual(cls | UserId, Union[cls, UserId])
+
+ self.assertEqual(get_args(UserId | cls), (UserId, cls))
+ self.assertEqual(get_args(cls | UserId), (cls, UserId))
+
+ def test_special_attrs(self):
+ self.assertEqual(UserId.__name__, 'UserId')
+ self.assertEqual(UserId.__qualname__, 'UserId')
+ self.assertEqual(UserId.__module__, __name__)
+ self.assertEqual(UserId.__supertype__, int)
+
+ UserName = self.UserName
+ self.assertEqual(UserName.__name__, 'UserName')
+ self.assertEqual(UserName.__qualname__,
+ self.__class__.__qualname__ + '.UserName')
+ self.assertEqual(UserName.__module__, __name__)
+ self.assertEqual(UserName.__supertype__, str)
+
+ def test_repr(self):
+ self.assertEqual(repr(UserId), f'{__name__}.UserId')
+ self.assertEqual(repr(self.UserName),
+ f'{__name__}.{self.__class__.__qualname__}.UserName')
+
+ def test_pickle(self):
+ UserAge = NewType('UserAge', float)
+ for proto in range(pickle.HIGHEST_PROTOCOL + 1):
+ with self.subTest(proto=proto):
+ pickled = pickle.dumps(UserId, proto)
+ loaded = pickle.loads(pickled)
+ self.assertIs(loaded, UserId)
+
+ pickled = pickle.dumps(self.UserName, proto)
+ loaded = pickle.loads(pickled)
+ self.assertIs(loaded, self.UserName)
+
+ with self.assertRaises(pickle.PicklingError):
+ pickle.dumps(UserAge, proto)
+
+ def test_missing__name__(self):
+ code = ("import typing_extensions\n"
+ "NT = typing_extensions.NewType('NT', int)\n"
+ )
+ exec(code, {})
+
+ def test_error_message_when_subclassing(self):
+ with self.assertRaisesRegex(
+ TypeError,
+ re.escape(
+ "Cannot subclass an instance of NewType. Perhaps you were looking for: "
+ "`ProUserId = NewType('ProUserId', UserId)`"
+ )
+ ):
+ class ProUserId(UserId):
+ ...
+
class Coordinate(Protocol):
x: int
@@ -3849,7 +3917,7 @@
if sys.version_info < (3, 10, 1):
exclude |= {"Literal"}
if sys.version_info < (3, 11):
- exclude |= {'final', 'Any'}
+ exclude |= {'final', 'Any', 'NewType'}
if sys.version_info < (3, 12):
exclude |= {
'Protocol', 'runtime_checkable', 'SupportsAbs', 'SupportsBytes',
diff --git a/src/typing_extensions.py b/src/typing_extensions.py
index cce31f8..dd12cfb 100644
--- a/src/typing_extensions.py
+++ b/src/typing_extensions.py
@@ -440,7 +440,6 @@
Counter = typing.Counter
ChainMap = typing.ChainMap
AsyncGenerator = typing.AsyncGenerator
-NewType = typing.NewType
Text = typing.Text
TYPE_CHECKING = typing.TYPE_CHECKING
@@ -2546,3 +2545,68 @@
raise TypeError(
f'Expected an instance of type, not {type(__cls).__name__!r}'
) from None
+
+
+# NewType is a class on Python 3.10+, making it pickleable
+# The error message for subclassing instances of NewType was improved on 3.11+
+if sys.version_info >= (3, 11):
+ NewType = typing.NewType
+else:
+ class NewType:
+ """NewType creates simple unique types with almost zero
+ runtime overhead. NewType(name, tp) is considered a subtype of tp
+ by static type checkers. At runtime, NewType(name, tp) returns
+ a dummy callable that simply returns its argument. Usage::
+ UserId = NewType('UserId', int)
+ def name_by_id(user_id: UserId) -> str:
+ ...
+ UserId('user') # Fails type check
+ name_by_id(42) # Fails type check
+ name_by_id(UserId(42)) # OK
+ num = UserId(5) + 1 # type: int
+ """
+
+ def __call__(self, obj):
+ return obj
+
+ def __init__(self, name, tp):
+ self.__qualname__ = name
+ if '.' in name:
+ name = name.rpartition('.')[-1]
+ self.__name__ = name
+ self.__supertype__ = tp
+ def_mod = _caller()
+ if def_mod != 'typing_extensions':
+ self.__module__ = def_mod
+
+ def __mro_entries__(self, bases):
+ # We defined __mro_entries__ to get a better error message
+ # if a user attempts to subclass a NewType instance. bpo-46170
+ supercls_name = self.__name__
+
+ class Dummy:
+ def __init_subclass__(cls):
+ subcls_name = cls.__name__
+ raise TypeError(
+ f"Cannot subclass an instance of NewType. "
+ f"Perhaps you were looking for: "
+ f"`{subcls_name} = NewType({subcls_name!r}, {supercls_name})`"
+ )
+
+ return (Dummy,)
+
+ def __repr__(self):
+ return f'{self.__module__}.{self.__qualname__}'
+
+ def __reduce__(self):
+ return self.__qualname__
+
+ if sys.version_info >= (3, 10):
+ # PEP 604 methods
+ # It doesn't make sense to have these methods on Python <3.10
+
+ def __or__(self, other):
+ return typing.Union[self, other]
+
+ def __ror__(self, other):
+ return typing.Union[other, self]