Skip to content

Commit

Permalink
add LiteralString (PEP 675) (#1053)
Browse files Browse the repository at this point in the history
Co-authored-by: Nikita Sobolev <mail@sobolevn.me>
  • Loading branch information
JelleZijlstra and sobolevn authored Feb 11, 2022
1 parent a53957c commit f6e8272
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 2 deletions.
1 change: 1 addition & 0 deletions typing_extensions/CHANGELOG
Original file line number Diff line number Diff line change
@@ -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).
Expand Down
1 change: 1 addition & 0 deletions typing_extensions/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
76 changes: 74 additions & 2 deletions typing_extensions/src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
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:
Expand Down Expand Up @@ -111,6 +111,11 @@ def test_cannot_instantiate(self):
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
Expand Down Expand Up @@ -1896,7 +1901,8 @@ def test_cannot_check_subclass(self):
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"]
Expand Down Expand Up @@ -2290,6 +2296,67 @@ def test_no_isinstance(self):
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:
Expand Down Expand Up @@ -2331,6 +2398,11 @@ class Alias:
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):
Expand Down
51 changes: 51 additions & 0 deletions typing_extensions/src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def _check_generic(cls, parameters):
'ClassVar',
'Concatenate',
'Final',
'LiteralString',
'ParamSpec',
'Self',
'Type',
Expand Down Expand Up @@ -2155,6 +2156,56 @@ def __getitem__(self, parameters):
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):
Expand Down

0 comments on commit f6e8272

Please sign in to comment.