From b612a78b5ea2dfc66ac93000044ad1461358f805 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 26 May 2016 23:54:04 +0200 Subject: [PATCH 1/5] Simple minded implementation of NewType --- python2/typing.py | 28 ++++++++++++++++++++++++++++ src/typing.py | 27 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/python2/typing.py b/python2/typing.py index ff6ba911..a3919443 100644 --- a/python2/typing.py +++ b/python2/typing.py @@ -1577,6 +1577,34 @@ def NamedTuple(typename, fields): return cls +def NewType(name, tp): + """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 function that simply returns its argument. Usage:: + + UserId = NewType('UserId', int) + + def name_by_id(user_id): + # type: (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 new_type(x): + return x + + new_type.__name__ = name + new_type.__supertype__ = tp + return new_type + + # Python-version-specific alias (Python 2: unicode; Python 3: str) Text = unicode diff --git a/src/typing.py b/src/typing.py index 4bd21354..0d50308a 100644 --- a/src/typing.py +++ b/src/typing.py @@ -1631,6 +1631,33 @@ def NamedTuple(typename, fields): return cls +def NewType(name, tp): + """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 function 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 new_type(x): + return x + + new_type.__name__ = name + new_type.__supertype__ = tp + return new_type + + # Python-version-specific alias (Python 2: unicode; Python 3: str) Text = str From 2146b2317ad7d844891b97396519d3105fb01d29 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 27 May 2016 00:29:53 +0200 Subject: [PATCH 2/5] Add unittests. Add NewType to __all__ --- python2/test_typing.py | 20 ++++++++++++++++++++ python2/typing.py | 4 +++- src/test_typing.py | 20 ++++++++++++++++++++ src/typing.py | 1 + 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/python2/test_typing.py b/python2/test_typing.py index b801383c..bf19da27 100644 --- a/python2/test_typing.py +++ b/python2/test_typing.py @@ -15,6 +15,7 @@ from typing import Generic from typing import cast from typing import Type +from typing import NewType from typing import NamedTuple from typing import IO, TextIO, BinaryIO from typing import Pattern, Match @@ -1141,6 +1142,25 @@ def new_user(user_class): joe = new_user(BasicUser) +class NewTypeTests(BaseTestCase): + + def test_basic(self): + UserId = NewType('UserId', int) + UserName = NewType('UserName', str) + self.assertIsInstance(UserId(5), int) + self.assertIsInstance(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): + pass + + class NamedTupleTests(BaseTestCase): def test_basics(self): diff --git a/python2/typing.py b/python2/typing.py index a3919443..0065251f 100644 --- a/python2/typing.py +++ b/python2/typing.py @@ -19,10 +19,12 @@ 'Any', 'Callable', 'Generic', + 'NewType', 'Optional', + 'Tuple', + 'Type', 'TypeVar', 'Union', - 'Tuple', # ABCs (from collections.abc). 'AbstractSet', # collections.abc.Set. diff --git a/src/test_typing.py b/src/test_typing.py index ade8a358..432bb158 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -16,6 +16,7 @@ from typing import get_type_hints from typing import no_type_check, no_type_check_decorator from typing import Type +from typing import NewType from typing import NamedTuple from typing import IO, TextIO, BinaryIO from typing import Pattern, Match @@ -1401,6 +1402,25 @@ def new_user(user_class: Type[U]) -> U: joe = new_user(BasicUser) +class NewTypeTests(BaseTestCase): + + def test_basic(self): + UserId = NewType('UserId', int) + UserName = NewType('UserName', str) + self.assertIsInstance(UserId(5), int) + self.assertIsInstance(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): + pass + + class NamedTupleTests(BaseTestCase): def test_basics(self): diff --git a/src/typing.py b/src/typing.py index 0d50308a..62a93f2b 100644 --- a/src/typing.py +++ b/src/typing.py @@ -18,6 +18,7 @@ 'Any', 'Callable', 'Generic', + 'NewType', 'Optional', 'Tuple', 'Type', From 473a394433c2a90936a8300d4198d270d68c14d6 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 27 May 2016 09:17:01 +0200 Subject: [PATCH 3/5] Short discusion in PEP plus corrections to lists --- pep-0484.txt | 47 ++++++++++++++++++++++++++++++++++++++++++ python2/test_typing.py | 2 +- python2/typing.py | 5 +++-- src/typing.py | 2 +- 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/pep-0484.txt b/pep-0484.txt index 781e23ef..7dde285a 100644 --- a/pep-0484.txt +++ b/pep-0484.txt @@ -1173,6 +1173,46 @@ checker should blindly believe the programmer. Also, casts can be used in expressions, while type comments only apply to assignments. +NewType helper function +----------------------- + +There are also situations, where a programmer might want to avoid logical +errors by creating simple classes. For example:: + + class UserId(int): + pass + + get_by_user_id(user_id: UserId): + ... + +However, this approach introduces a runtime overhead. To avoid this, +``typing.py`` provides a helper function ``NewType`` that creates +simple unique types with almost zero runtime overhead. For a static type +checker ``Derived = NewType('Derived', Base)`` is roughly equivalent +to a definition:: + +class Derived(Base): + def __init__(self, _x: Base) -> None: + ... + +While at runtime, ``NewType('Derived', Base)`` returns a dummy function +that simply returns its argument. Type checkers require explicit casts +from ``int`` where ``UserId`` is expected, while implicitly casting +from ``UserId`` where ``int`` is expected. Examples:: + + 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 + + Stub Files ========== @@ -1454,6 +1494,8 @@ Fundamental building blocks: * Generic, used to create user-defined generic classes +* Type, used to annotate class objects + Generic variants of builtin collections: * Dict, used as ``Dict[key_type, value_type]`` @@ -1487,6 +1529,8 @@ Generic variants of container ABCs (and a few non-containers): * Container +* ContextManager + * Generator, used as ``Generator[yield_type, send_type, return_type]``. This represents the return value of generator functions. It is a subtype of ``Iterable`` and it has additional @@ -1558,6 +1602,9 @@ Convenience definitions: This is useful to declare the types of the fields of a named tuple type. +* NewType, used to create unique types with little runtime overhead + ``UserId = NewType('UserId', int)`` + * cast(), described earlier * @no_type_check, a decorator to disable type checking per class or diff --git a/python2/test_typing.py b/python2/test_typing.py index bf19da27..7cce3a54 100644 --- a/python2/test_typing.py +++ b/python2/test_typing.py @@ -1148,7 +1148,7 @@ def test_basic(self): UserId = NewType('UserId', int) UserName = NewType('UserName', str) self.assertIsInstance(UserId(5), int) - self.assertIsInstance(UserName('Joe'), str) + self.assertIsInstance(UserName('Joe'), type('Joe')) self.assertEqual(UserId(5) + 1, 6) def test_errors(self): diff --git a/python2/typing.py b/python2/typing.py index 0065251f..35edb629 100644 --- a/python2/typing.py +++ b/python2/typing.py @@ -19,7 +19,6 @@ 'Any', 'Callable', 'Generic', - 'NewType', 'Optional', 'Tuple', 'Type', @@ -62,6 +61,7 @@ 'AnyStr', 'cast', 'get_type_hints', + 'NewType', 'no_type_check', 'no_type_check_decorator', 'overload', @@ -1602,7 +1602,8 @@ def name_by_id(user_id): def new_type(x): return x - new_type.__name__ = name + # Some versions of Python 2 complain because of making all strings unicode + new_type.__name__ = str(name) new_type.__supertype__ = tp return new_type diff --git a/src/typing.py b/src/typing.py index 62a93f2b..17b28c77 100644 --- a/src/typing.py +++ b/src/typing.py @@ -18,7 +18,6 @@ 'Any', 'Callable', 'Generic', - 'NewType', 'Optional', 'Tuple', 'Type', @@ -65,6 +64,7 @@ 'AnyStr', 'cast', 'get_type_hints', + 'NewType', 'no_type_check', 'no_type_check_decorator', 'overload', From 3c6371ce7934bdf57ede96064a5054425adff068 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 27 May 2016 20:49:32 +0200 Subject: [PATCH 4/5] response to comments on NewType --- pep-0484.txt | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/pep-0484.txt b/pep-0484.txt index 7dde285a..460a4f4e 100644 --- a/pep-0484.txt +++ b/pep-0484.txt @@ -1176,7 +1176,7 @@ in expressions, while type comments only apply to assignments. NewType helper function ----------------------- -There are also situations, where a programmer might want to avoid logical +There are also situations where a programmer might want to avoid logical errors by creating simple classes. For example:: class UserId(int): @@ -1212,6 +1212,24 @@ from ``UserId`` where ``int`` is expected. Examples:: num = UserId(5) + 1 # type: int +``NewType`` accepts only one argument that shoud be a proper class, +i.e., not a type construct like ``Union``, etc. Function returned +by ``NewType`` accepts only one argument, this is equivalent to supporting +only one constructor accepting an instace of the base class (see above). +Example:: + + class PacketId: + def __init__(self, major: int, minor: int) -> None: + self._major = major + self._minor = minor + + TcpPacketId = NewType('TcpPacketId', PacketId) + + packet = PacketId(100, 100) + tcp_packet = TcpPacketId(packet) # OK + + tcp_packet = TcpPacketId(127, 0) # This fails + Stub Files ========== From 060516309909e5dad32ef6edf54f426411afb8ea Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 27 May 2016 22:42:00 +0200 Subject: [PATCH 5/5] Response to further comments --- pep-0484.txt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pep-0484.txt b/pep-0484.txt index 460a4f4e..97920f3d 100644 --- a/pep-0484.txt +++ b/pep-0484.txt @@ -1213,9 +1213,9 @@ from ``UserId`` where ``int`` is expected. Examples:: num = UserId(5) + 1 # type: int ``NewType`` accepts only one argument that shoud be a proper class, -i.e., not a type construct like ``Union``, etc. Function returned -by ``NewType`` accepts only one argument, this is equivalent to supporting -only one constructor accepting an instace of the base class (see above). +i.e., not a type construct like ``Union``, etc. The function returned +by ``NewType`` accepts only one argument; this is equivalent to supporting +only one constructor accepting an instance of the base class (see above). Example:: class PacketId: @@ -1228,7 +1228,11 @@ Example:: packet = PacketId(100, 100) tcp_packet = TcpPacketId(packet) # OK - tcp_packet = TcpPacketId(127, 0) # This fails + tcp_packet = TcpPacketId(127, 0) # Fails in type checker and at runtime + +Both ``isinstance`` and ``issubclass``, as well as subclassing will fail +for ``NewType('Derived', Base)`` since function objects don't support +these operations. Stub Files