Skip to content

Commit ecdae75

Browse files
Merge branch 'main' into extra
2 parents b05b4a5 + 9f040ab commit ecdae75

File tree

7 files changed

+173
-23
lines changed

7 files changed

+173
-23
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262
- uses: actions/checkout@v4
6363

6464
- name: Set up Python
65-
uses: actions/setup-python@v4
65+
uses: actions/setup-python@v5
6666
with:
6767
python-version: ${{ matrix.python-version }}
6868
allow-prereleases: true
@@ -85,7 +85,7 @@ jobs:
8585
steps:
8686
- uses: actions/checkout@v4
8787
- name: Set up Python
88-
uses: actions/setup-python@v4
88+
uses: actions/setup-python@v5
8989
with:
9090
python-version: "3"
9191
cache: "pip"
@@ -122,7 +122,7 @@ jobs:
122122
issues: write
123123

124124
steps:
125-
- uses: actions/github-script@v6
125+
- uses: actions/github-script@v7
126126
with:
127127
github-token: ${{ secrets.GITHUB_TOKEN }}
128128
script: |

.github/workflows/package.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- uses: actions/checkout@v4
2424

2525
- name: Set up Python
26-
uses: actions/setup-python@v4
26+
uses: actions/setup-python@v5
2727
with:
2828
python-version: 3
2929

@@ -53,7 +53,7 @@ jobs:
5353
- uses: actions/checkout@v4
5454

5555
- name: Set up Python
56-
uses: actions/setup-python@v4
56+
uses: actions/setup-python@v5
5757
with:
5858
python-version: 3
5959

.github/workflows/third_party.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ jobs:
100100
with:
101101
path: typing-extensions-latest
102102
- name: Setup Python
103-
uses: actions/setup-python@v4
103+
uses: actions/setup-python@v5
104104
with:
105105
python-version: ${{ matrix.python-version }}
106106
- name: Install typing_inspect test dependencies
@@ -143,7 +143,7 @@ jobs:
143143
with:
144144
path: typing-extensions-latest
145145
- name: Setup Python
146-
uses: actions/setup-python@v4
146+
uses: actions/setup-python@v5
147147
with:
148148
python-version: ${{ matrix.python-version }}
149149
allow-prereleases: true
@@ -187,7 +187,7 @@ jobs:
187187
with:
188188
path: typing-extensions-latest
189189
- name: Setup Python
190-
uses: actions/setup-python@v4
190+
uses: actions/setup-python@v5
191191
with:
192192
python-version: ${{ matrix.python-version }}
193193
allow-prereleases: true
@@ -231,7 +231,7 @@ jobs:
231231
with:
232232
path: typing-extensions-latest
233233
- name: Setup Python
234-
uses: actions/setup-python@v4
234+
uses: actions/setup-python@v5
235235
with:
236236
python-version: ${{ matrix.python-version }}
237237
- name: Configure git for typed-argument-parser tests
@@ -282,7 +282,7 @@ jobs:
282282
with:
283283
path: typing-extensions-latest
284284
- name: Setup Python
285-
uses: actions/setup-python@v4
285+
uses: actions/setup-python@v5
286286
with:
287287
python-version: ${{ matrix.python-version }}
288288
allow-prereleases: true
@@ -328,7 +328,7 @@ jobs:
328328
with:
329329
path: typing-extensions-latest
330330
- name: Setup Python
331-
uses: actions/setup-python@v4
331+
uses: actions/setup-python@v5
332332
with:
333333
python-version: ${{ matrix.python-version }}
334334
- name: Install pdm for cattrs
@@ -377,7 +377,7 @@ jobs:
377377
issues: write
378378

379379
steps:
380-
- uses: actions/github-script@v6
380+
- uses: actions/github-script@v7
381381
with:
382382
github-token: ${{ secrets.GITHUB_TOKEN }}
383383
script: |

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
- Add support for PEP 728, making `__extra__` a reserved key for TypedDict.
44
Patch by Zixuan James Li.
5+
- Add support for PEP 742, adding `typing_extensions.TypeIs`. Patch
6+
by Jelle Zijlstra.
7+
- Drop runtime error when a read-only `TypedDict` item overrides a mutable
8+
one. Type checkers should still flag this as an error. Patch by Jelle
9+
Zijlstra.
510
- Speedup `issubclass()` checks against simple runtime-checkable protocols by
611
around 6% (backporting https://github.com/python/cpython/pull/112717, by Alex
712
Waygood).

doc/index.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,12 @@ Special typing primitives
350350

351351
See :py:data:`typing.TypeGuard` and :pep:`647`. In ``typing`` since 3.10.
352352

353+
.. data:: TypeIs
354+
355+
See :pep:`742`. Similar to :data:`TypeGuard`, but allows more type narrowing.
356+
357+
.. versionadded:: 4.10.0
358+
353359
.. class:: TypedDict(dict, total=True)
354360

355361
See :py:class:`typing.TypedDict` and :pep:`589`. In ``typing`` since 3.8.

src/test_typing_extensions.py

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from typing_extensions import TypeVarTuple, Unpack, dataclass_transform, reveal_type, Never, assert_never, LiteralString
3737
from typing_extensions import assert_type, get_type_hints, get_origin, get_args, get_original_bases
3838
from typing_extensions import clear_overloads, get_overloads, overload
39-
from typing_extensions import NamedTuple
39+
from typing_extensions import NamedTuple, TypeIs
4040
from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar, get_protocol_members, is_protocol
4141
from typing_extensions import Doc
4242
from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated
@@ -3553,7 +3553,8 @@ def test_basics_functional_syntax(self):
35533553
@skipIf(sys.version_info < (3, 13), "Change in behavior in 3.13")
35543554
def test_keywords_syntax_raises_on_3_13(self):
35553555
with self.assertRaises(TypeError):
3556-
Emp = TypedDict('Emp', name=str, id=int)
3556+
with self.assertWarns(DeprecationWarning):
3557+
Emp = TypedDict('Emp', name=str, id=int)
35573558

35583559
@skipIf(sys.version_info >= (3, 13), "3.13 removes support for kwargs")
35593560
def test_basics_keywords_syntax(self):
@@ -4142,13 +4143,18 @@ class Child2(Base2):
41424143
self.assertEqual(Child1.__readonly_keys__, frozenset({'a'}))
41434144
self.assertEqual(Child1.__mutable_keys__, frozenset({'b'}))
41444145

4145-
def test_cannot_make_mutable_key_readonly(self):
4146+
def test_make_mutable_key_readonly(self):
41464147
class Base(TypedDict):
41474148
a: int
41484149

4149-
with self.assertRaises(TypeError):
4150-
class Child(Base):
4151-
a: ReadOnly[int]
4150+
self.assertEqual(Base.__readonly_keys__, frozenset())
4151+
self.assertEqual(Base.__mutable_keys__, frozenset({'a'}))
4152+
4153+
class Child(Base):
4154+
a: ReadOnly[int] # type checker error, but allowed at runtime
4155+
4156+
self.assertEqual(Child.__readonly_keys__, frozenset({'a'}))
4157+
self.assertEqual(Child.__mutable_keys__, frozenset())
41524158

41534159
def test_can_make_readonly_key_mutable(self):
41544160
class Base(TypedDict):
@@ -4811,6 +4817,50 @@ def test_no_isinstance(self):
48114817
issubclass(int, TypeGuard)
48124818

48134819

4820+
class TypeIsTests(BaseTestCase):
4821+
def test_basics(self):
4822+
TypeIs[int] # OK
4823+
self.assertEqual(TypeIs[int], TypeIs[int])
4824+
4825+
def foo(arg) -> TypeIs[int]: ...
4826+
self.assertEqual(gth(foo), {'return': TypeIs[int]})
4827+
4828+
def test_repr(self):
4829+
if hasattr(typing, 'TypeIs'):
4830+
mod_name = 'typing'
4831+
else:
4832+
mod_name = 'typing_extensions'
4833+
self.assertEqual(repr(TypeIs), f'{mod_name}.TypeIs')
4834+
cv = TypeIs[int]
4835+
self.assertEqual(repr(cv), f'{mod_name}.TypeIs[int]')
4836+
cv = TypeIs[Employee]
4837+
self.assertEqual(repr(cv), f'{mod_name}.TypeIs[{__name__}.Employee]')
4838+
cv = TypeIs[Tuple[int]]
4839+
self.assertEqual(repr(cv), f'{mod_name}.TypeIs[typing.Tuple[int]]')
4840+
4841+
def test_cannot_subclass(self):
4842+
with self.assertRaises(TypeError):
4843+
class C(type(TypeIs)):
4844+
pass
4845+
with self.assertRaises(TypeError):
4846+
class C(type(TypeIs[int])):
4847+
pass
4848+
4849+
def test_cannot_init(self):
4850+
with self.assertRaises(TypeError):
4851+
TypeIs()
4852+
with self.assertRaises(TypeError):
4853+
type(TypeIs)()
4854+
with self.assertRaises(TypeError):
4855+
type(TypeIs[Optional[int]])()
4856+
4857+
def test_no_isinstance(self):
4858+
with self.assertRaises(TypeError):
4859+
isinstance(1, TypeIs[int])
4860+
with self.assertRaises(TypeError):
4861+
issubclass(int, TypeIs)
4862+
4863+
48144864
class LiteralStringTests(BaseTestCase):
48154865
def test_basics(self):
48164866
class Foo:

src/typing_extensions.py

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
'TypeAlias',
8484
'TypeAliasType',
8585
'TypeGuard',
86+
'TypeIs',
8687
'TYPE_CHECKING',
8788
'Never',
8889
'NoReturn',
@@ -949,11 +950,7 @@ def __new__(cls, name, bases, ns, *, total=True):
949950
else:
950951
optional_keys.add(annotation_key)
951952
if ReadOnly in qualifiers:
952-
if annotation_key in mutable_keys:
953-
raise TypeError(
954-
f"Cannot override mutable key {annotation_key!r}"
955-
" with read-only key"
956-
)
953+
mutable_keys.discard(annotation_key)
957954
readonly_keys.add(annotation_key)
958955
else:
959956
mutable_keys.add(annotation_key)
@@ -1834,6 +1831,98 @@ def is_str(val: Union[str, float]):
18341831
PEP 647 (User-Defined Type Guards).
18351832
""")
18361833

1834+
# 3.13+
1835+
if hasattr(typing, 'TypeIs'):
1836+
TypeIs = typing.TypeIs
1837+
# 3.9
1838+
elif sys.version_info[:2] >= (3, 9):
1839+
@_ExtensionsSpecialForm
1840+
def TypeIs(self, parameters):
1841+
"""Special typing form used to annotate the return type of a user-defined
1842+
type narrower function. ``TypeIs`` only accepts a single type argument.
1843+
At runtime, functions marked this way should return a boolean.
1844+
1845+
``TypeIs`` aims to benefit *type narrowing* -- a technique used by static
1846+
type checkers to determine a more precise type of an expression within a
1847+
program's code flow. Usually type narrowing is done by analyzing
1848+
conditional code flow and applying the narrowing to a block of code. The
1849+
conditional expression here is sometimes referred to as a "type guard".
1850+
1851+
Sometimes it would be convenient to use a user-defined boolean function
1852+
as a type guard. Such a function should use ``TypeIs[...]`` as its
1853+
return type to alert static type checkers to this intention.
1854+
1855+
Using ``-> TypeIs`` tells the static type checker that for a given
1856+
function:
1857+
1858+
1. The return value is a boolean.
1859+
2. If the return value is ``True``, the type of its argument
1860+
is the intersection of the type inside ``TypeGuard`` and the argument's
1861+
previously known type.
1862+
1863+
For example::
1864+
1865+
def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]:
1866+
return hasattr(val, '__await__')
1867+
1868+
def f(val: Union[int, Awaitable[int]]) -> int:
1869+
if is_awaitable(val):
1870+
assert_type(val, Awaitable[int])
1871+
else:
1872+
assert_type(val, int)
1873+
1874+
``TypeIs`` also works with type variables. For more information, see
1875+
PEP 742 (Narrowing types with TypeIs).
1876+
"""
1877+
item = typing._type_check(parameters, f'{self} accepts only a single type.')
1878+
return typing._GenericAlias(self, (item,))
1879+
# 3.8
1880+
else:
1881+
class _TypeIsForm(_ExtensionsSpecialForm, _root=True):
1882+
def __getitem__(self, parameters):
1883+
item = typing._type_check(parameters,
1884+
f'{self._name} accepts only a single type')
1885+
return typing._GenericAlias(self, (item,))
1886+
1887+
TypeIs = _TypeIsForm(
1888+
'TypeIs',
1889+
doc="""Special typing form used to annotate the return type of a user-defined
1890+
type narrower function. ``TypeIs`` only accepts a single type argument.
1891+
At runtime, functions marked this way should return a boolean.
1892+
1893+
``TypeIs`` aims to benefit *type narrowing* -- a technique used by static
1894+
type checkers to determine a more precise type of an expression within a
1895+
program's code flow. Usually type narrowing is done by analyzing
1896+
conditional code flow and applying the narrowing to a block of code. The
1897+
conditional expression here is sometimes referred to as a "type guard".
1898+
1899+
Sometimes it would be convenient to use a user-defined boolean function
1900+
as a type guard. Such a function should use ``TypeIs[...]`` as its
1901+
return type to alert static type checkers to this intention.
1902+
1903+
Using ``-> TypeIs`` tells the static type checker that for a given
1904+
function:
1905+
1906+
1. The return value is a boolean.
1907+
2. If the return value is ``True``, the type of its argument
1908+
is the intersection of the type inside ``TypeGuard`` and the argument's
1909+
previously known type.
1910+
1911+
For example::
1912+
1913+
def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]:
1914+
return hasattr(val, '__await__')
1915+
1916+
def f(val: Union[int, Awaitable[int]]) -> int:
1917+
if is_awaitable(val):
1918+
assert_type(val, Awaitable[int])
1919+
else:
1920+
assert_type(val, int)
1921+
1922+
``TypeIs`` also works with type variables. For more information, see
1923+
PEP 742 (Narrowing types with TypeIs).
1924+
""")
1925+
18371926

18381927
# Vendored from cpython typing._SpecialFrom
18391928
class _SpecialForm(typing._Final, _root=True):

0 commit comments

Comments
 (0)