aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJelle Zijlstra <jelle.zijlstra@gmail.com>2022-02-10 18:21:27 -0800
committerGitHub <noreply@github.com>2022-02-10 18:21:27 -0800
commitf6e827230b397c26a114638fd5cac54517905d4d (patch)
tree55440a137693e47fcbc8e85f8ececce27b0fe680
parenta53957cad88444d9a33e5906cef31c39fb0b919a (diff)
downloadtyping-f6e827230b397c26a114638fd5cac54517905d4d.tar.gz
add LiteralString (PEP 675) (#1053)
Co-authored-by: Nikita Sobolev <mail@sobolevn.me>
-rw-r--r--typing_extensions/CHANGELOG1
-rw-r--r--typing_extensions/README.rst1
-rw-r--r--typing_extensions/src/test_typing_extensions.py76
-rw-r--r--typing_extensions/src/typing_extensions.py51
4 files changed, 127 insertions, 2 deletions
diff --git a/typing_extensions/CHANGELOG b/typing_extensions/CHANGELOG
index 092e04a..b874ddc 100644
--- a/typing_extensions/CHANGELOG
+++ b/typing_extensions/CHANGELOG
@@ -1,5 +1,6 @@
# Release 4.x.x
+- Runtime support for PEP 675 and `typing_extensions.LiteralString`.
- Add `Never` and `assert_never`. Backport from bpo-46475.
- `ParamSpec` args and kwargs are now equal to themselves. Backport from
bpo-46676. Patch by Gregory Beauregard (@GBeauregard).
diff --git a/typing_extensions/README.rst b/typing_extensions/README.rst
index a83ed3c..4430d66 100644
--- a/typing_extensions/README.rst
+++ b/typing_extensions/README.rst
@@ -37,6 +37,7 @@ This module currently contains the following:
- Experimental features
+ - ``LiteralString`` (see PEP 675)
- ``@dataclass_transform()`` (see PEP 681)
- ``NotRequired`` (see PEP 655)
- ``Required`` (see PEP 655)
diff --git a/typing_extensions/src/test_typing_extensions.py b/typing_extensions/src/test_typing_extensions.py
index 9111688..c4db70e 100644
--- a/typing_extensions/src/test_typing_extensions.py
+++ b/typing_extensions/src/test_typing_extensions.py
@@ -22,7 +22,7 @@ from typing_extensions import NoReturn, ClassVar, Final, IntVar, Literal, Type,
from typing_extensions import TypeAlias, ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs, TypeGuard
from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired
from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, overload, final, is_typeddict
-from typing_extensions import dataclass_transform, reveal_type, Never, assert_never
+from typing_extensions import dataclass_transform, reveal_type, Never, assert_never, LiteralString
try:
from typing_extensions import get_type_hints
except ImportError:
@@ -111,6 +111,11 @@ class BottomTypeTestsMixin:
with self.assertRaises(TypeError):
type(self.bottom_type)()
+ def test_pickle(self):
+ for proto in range(pickle.HIGHEST_PROTOCOL):
+ pickled = pickle.dumps(self.bottom_type, protocol=proto)
+ self.assertIs(self.bottom_type, pickle.loads(pickled))
+
class NoReturnTests(BottomTypeTestsMixin, BaseTestCase):
bottom_type = NoReturn
@@ -1896,7 +1901,8 @@ class AnnotatedTests(BaseTestCase):
def test_pickle(self):
samples = [typing.Any, typing.Union[int, str],
typing.Optional[str], Tuple[int, ...],
- typing.Callable[[str], bytes]]
+ typing.Callable[[str], bytes],
+ Self, LiteralString, Never]
for t in samples:
x = Annotated[t, "a"]
@@ -2290,6 +2296,67 @@ class TypeGuardTests(BaseTestCase):
issubclass(int, TypeGuard)
+class LiteralStringTests(BaseTestCase):
+ def test_basics(self):
+ class Foo:
+ def bar(self) -> LiteralString: ...
+ def baz(self) -> "LiteralString": ...
+
+ self.assertEqual(gth(Foo.bar), {'return': LiteralString})
+ self.assertEqual(gth(Foo.baz), {'return': LiteralString})
+
+ @skipUnless(PEP_560, "Python 3.7+ required")
+ def test_get_origin(self):
+ from typing_extensions import get_origin
+ self.assertIsNone(get_origin(LiteralString))
+
+ def test_repr(self):
+ if hasattr(typing, 'LiteralString'):
+ mod_name = 'typing'
+ else:
+ mod_name = 'typing_extensions'
+ self.assertEqual(repr(LiteralString), '{}.LiteralString'.format(mod_name))
+
+ def test_cannot_subscript(self):
+ with self.assertRaises(TypeError):
+ LiteralString[int]
+
+ def test_cannot_subclass(self):
+ with self.assertRaises(TypeError):
+ class C(type(LiteralString)):
+ pass
+ with self.assertRaises(TypeError):
+ class C(LiteralString):
+ pass
+
+ def test_cannot_init(self):
+ with self.assertRaises(TypeError):
+ LiteralString()
+ with self.assertRaises(TypeError):
+ type(LiteralString)()
+
+ def test_no_isinstance(self):
+ with self.assertRaises(TypeError):
+ isinstance(1, LiteralString)
+ with self.assertRaises(TypeError):
+ issubclass(int, LiteralString)
+
+ def test_alias(self):
+ StringTuple = Tuple[LiteralString, LiteralString]
+ class Alias:
+ def return_tuple(self) -> StringTuple:
+ return ("foo", "pep" + "675")
+
+ def test_typevar(self):
+ StrT = TypeVar("StrT", bound=LiteralString)
+ self.assertIs(StrT.__bound__, LiteralString)
+
+ def test_pickle(self):
+ for proto in range(pickle.HIGHEST_PROTOCOL):
+ pickled = pickle.dumps(LiteralString, protocol=proto)
+ self.assertIs(LiteralString, pickle.loads(pickled))
+
+
class SelfTests(BaseTestCase):
def test_basics(self):
class Foo:
@@ -2331,6 +2398,11 @@ class SelfTests(BaseTestCase):
def return_tuple(self) -> TupleSelf:
return (self, self)
+ def test_pickle(self):
+ for proto in range(pickle.HIGHEST_PROTOCOL):
+ pickled = pickle.dumps(Self, protocol=proto)
+ self.assertIs(Self, pickle.loads(pickled))
+
class FinalDecoratorTests(BaseTestCase):
def test_final_unmodified(self):
diff --git a/typing_extensions/src/typing_extensions.py b/typing_extensions/src/typing_extensions.py
index b67efd0..d0bcc32 100644
--- a/typing_extensions/src/typing_extensions.py
+++ b/typing_extensions/src/typing_extensions.py
@@ -44,6 +44,7 @@ __all__ = [
'ClassVar',
'Concatenate',
'Final',
+ 'LiteralString',
'ParamSpec',
'Self',
'Type',
@@ -2155,6 +2156,56 @@ if sys.version_info[:2] >= (3, 7):
return self._getitem(self, parameters)
+if hasattr(typing, "LiteralString"):
+ LiteralString = typing.LiteralString
+elif sys.version_info[:2] >= (3, 7):
+ @_SpecialForm
+ def LiteralString(self, params):
+ """Represents an arbitrary literal string.
+
+ Example::
+
+ from typing_extensions import LiteralString
+
+ def query(sql: LiteralString) -> ...:
+ ...
+
+ query("SELECT * FROM table") # ok
+ query(f"SELECT * FROM {input()}") # not ok
+
+ See PEP 675 for details.
+
+ """
+ raise TypeError(f"{self} is not subscriptable")
+else:
+ class _LiteralString(typing._FinalTypingBase, _root=True):
+ """Represents an arbitrary literal string.
+
+ Example::
+
+ from typing_extensions import LiteralString
+
+ def query(sql: LiteralString) -> ...:
+ ...
+
+ query("SELECT * FROM table") # ok
+ query(f"SELECT * FROM {input()}") # not ok
+
+ See PEP 675 for details.
+
+ """
+
+ __slots__ = ()
+
+ def __instancecheck__(self, obj):
+ raise TypeError(f"{self} cannot be used with isinstance().")
+
+ def __subclasscheck__(self, cls):
+ raise TypeError(f"{self} cannot be used with issubclass().")
+
+ LiteralString = _LiteralString(_root=True)
+
+
if hasattr(typing, "Self"):
Self = typing.Self
elif sys.version_info[:2] >= (3, 7):