Skip to content

Commit

Permalink
Backport NewType as it exists on py310+ (python#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexWaygood authored and ShaneMurphy2 committed May 17, 2023
1 parent 3c0310f commit a7dad84
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 8 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,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)

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ Certain objects were changed after they were added to `typing`, and
`isinstance()` checks against all these protocols were sped up significantly
on Python 3.12. `typing_extensions` backports the faster versions to Python
3.7+.
- `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
Expand Down
82 changes: 75 additions & 7 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import importlib
import inspect
import pickle
import re
import subprocess
import tempfile
import types
Expand Down Expand Up @@ -1537,23 +1538,90 @@ def foo(a: A) -> Optional[BaseException]:


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
Expand Down Expand Up @@ -3847,7 +3915,7 @@ def test_typing_extensions_defers_when_possible(self):
if sys.version_info < (3, 10):
exclude |= {'get_args', 'get_origin'}
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',
Expand Down
66 changes: 65 additions & 1 deletion src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,6 @@ def clear_overloads():
Counter = typing.Counter
ChainMap = typing.ChainMap
AsyncGenerator = typing.AsyncGenerator
NewType = typing.NewType
Text = typing.Text
TYPE_CHECKING = typing.TYPE_CHECKING

Expand Down Expand Up @@ -2546,3 +2545,68 @@ class Baz(list[str]): ...
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]

0 comments on commit a7dad84

Please sign in to comment.