From b891c465bb7d38a597c5c2ad547d7b19194f4dad Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 26 May 2019 09:37:48 +0100 Subject: [PATCH] bpo-37046: PEP 586: Add Literal to typing module (#13572) The implementation is straightforward and essentially is just copied from `typing_extensions`. --- Doc/library/typing.rst | 22 +++++++ Lib/test/test_typing.py | 64 ++++++++++++++++++- Lib/typing.py | 27 ++++++++ .../2019-05-25-19-12-53.bpo-37046.iuhQQj.rst | 1 + 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2019-05-25-19-12-53.bpo-37046.iuhQQj.rst diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 8362f1d8e6b..e64fecb8544 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1103,6 +1103,28 @@ The module defines the following classes, functions and decorators: ``Callable[..., Any]``, and in turn to :class:`collections.abc.Callable`. +.. data:: Literal + + A type that can be used to indicate to type checkers that the + corresponding variable or function parameter has a value equivalent to + the provided literal (or one of several literals). For example:: + + def validate_simple(data: Any) -> Literal[True]: # always returns True + ... + + MODE = Literal['r', 'rb', 'w', 'wb'] + def open_helper(file: str, mode: MODE) -> str: + ... + + open_helper('/some/path', 'r') # Passes type check + open_helper('/other/path', 'typo') # Error in type checker + + ``Literal[...]`` cannot be subclassed. At runtime, an arbitrary value + is allowed as type argument to ``Literal[...]``, but type checkers may + impose restrictions. See :pep:`586` for more details about literal types. + + .. versionadded:: 3.8 + .. data:: ClassVar Special type construct to mark class variables. diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 3d93eb396ce..eb618936d97 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -9,7 +9,7 @@ from copy import copy, deepcopy from typing import Any, NoReturn from typing import TypeVar, AnyStr from typing import T, KT, VT # Not in __all__. -from typing import Union, Optional +from typing import Union, Optional, Literal from typing import Tuple, List, MutableMapping from typing import Callable from typing import Generic, ClassVar, Final, final @@ -489,6 +489,68 @@ class CallableTests(BaseTestCase): typing.List[Callable[..., str]] +class LiteralTests(BaseTestCase): + def test_basics(self): + # All of these are allowed. + Literal[1] + Literal[1, 2, 3] + Literal["x", "y", "z"] + Literal[None] + Literal[True] + Literal[1, "2", False] + Literal[Literal[1, 2], Literal[4, 5]] + Literal[b"foo", u"bar"] + + def test_illegal_parameters_do_not_raise_runtime_errors(self): + # Type checkers should reject these types, but we do not + # raise errors at runtime to maintain maximium flexibility. + Literal[int] + Literal[3j + 2, ..., ()] + Literal[{"foo": 3, "bar": 4}] + Literal[T] + + def test_literals_inside_other_types(self): + List[Literal[1, 2, 3]] + List[Literal[("foo", "bar", "baz")]] + + def test_repr(self): + self.assertEqual(repr(Literal[1]), "typing.Literal[1]") + self.assertEqual(repr(Literal[1, True, "foo"]), "typing.Literal[1, True, 'foo']") + self.assertEqual(repr(Literal[int]), "typing.Literal[int]") + self.assertEqual(repr(Literal), "typing.Literal") + self.assertEqual(repr(Literal[None]), "typing.Literal[None]") + + def test_cannot_init(self): + with self.assertRaises(TypeError): + Literal() + with self.assertRaises(TypeError): + Literal[1]() + with self.assertRaises(TypeError): + type(Literal)() + with self.assertRaises(TypeError): + type(Literal[1])() + + def test_no_isinstance_or_issubclass(self): + with self.assertRaises(TypeError): + isinstance(1, Literal[1]) + with self.assertRaises(TypeError): + isinstance(int, Literal[1]) + with self.assertRaises(TypeError): + issubclass(1, Literal[1]) + with self.assertRaises(TypeError): + issubclass(int, Literal[1]) + + def test_no_subclassing(self): + with self.assertRaises(TypeError): + class Foo(Literal[1]): pass + with self.assertRaises(TypeError): + class Bar(Literal): pass + + def test_no_multiple_subscripts(self): + with self.assertRaises(TypeError): + Literal[1][1] + + XK = TypeVar('XK', str, bytes) XV = TypeVar('XV') diff --git a/Lib/typing.py b/Lib/typing.py index 06a7eb0dff8..1044cc409f4 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -37,6 +37,7 @@ __all__ = [ 'ClassVar', 'Final', 'Generic', + 'Literal', 'Optional', 'Tuple', 'Type', @@ -355,6 +356,10 @@ class _SpecialForm(_Final, _Immutable, _root=True): if self._name == 'Optional': arg = _type_check(parameters, "Optional[t] requires a single type.") return Union[arg, type(None)] + if self._name == 'Literal': + # There is no '_type_check' call because arguments to Literal[...] are + # values, not types. + return _GenericAlias(self, parameters) raise TypeError(f"{self} is not subscriptable") @@ -451,6 +456,28 @@ Optional = _SpecialForm('Optional', doc= Optional[X] is equivalent to Union[X, None]. """) +Literal = _SpecialForm('Literal', doc= + """Special typing form to define literal types (a.k.a. value types). + + This form can be used to indicate to type checkers that the corresponding + variable or function parameter has a value equivalent to the provided + literal (or one of several literals): + + def validate_simple(data: Any) -> Literal[True]: # always returns True + ... + + MODE = Literal['r', 'rb', 'w', 'wb'] + def open_helper(file: str, mode: MODE) -> str: + ... + + open_helper('/some/path', 'r') # Passes type check + open_helper('/other/path', 'typo') # Error in type checker + + Literal[...] cannot be subclassed. At runtime, an arbitrary value + is allowed as type argument to Literal[...], but type checkers may + impose restrictions. + """) + class ForwardRef(_Final, _root=True): """Internal wrapper to hold a forward reference.""" diff --git a/Misc/NEWS.d/next/Library/2019-05-25-19-12-53.bpo-37046.iuhQQj.rst b/Misc/NEWS.d/next/Library/2019-05-25-19-12-53.bpo-37046.iuhQQj.rst new file mode 100644 index 00000000000..9ec333b2d98 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-05-25-19-12-53.bpo-37046.iuhQQj.rst @@ -0,0 +1 @@ +PEP 586: Add ``Literal`` to the ``typing`` module. \ No newline at end of file