This commit is contained in:
Thomas Wouters 2024-04-09 20:42:07 +02:00
commit d0f93d132f
21 changed files with 551 additions and 95 deletions

View File

@ -392,6 +392,27 @@ is also included in the exception group.
The same special case is made for The same special case is made for
:exc:`KeyboardInterrupt` and :exc:`SystemExit` as in the previous paragraph. :exc:`KeyboardInterrupt` and :exc:`SystemExit` as in the previous paragraph.
Task groups are careful not to mix up the internal cancellation used to
"wake up" their :meth:`~object.__aexit__` with cancellation requests
for the task in which they are running made by other parties.
In particular, when one task group is syntactically nested in another,
and both experience an exception in one of their child tasks simultaneously,
the inner task group will process its exceptions, and then the outer task group
will receive another cancellation and process its own exceptions.
In the case where a task group is cancelled externally and also must
raise an :exc:`ExceptionGroup`, it will call the parent task's
:meth:`~asyncio.Task.cancel` method. This ensures that a
:exc:`asyncio.CancelledError` will be raised at the next
:keyword:`await`, so the cancellation is not lost.
Task groups preserve the cancellation count
reported by :meth:`asyncio.Task.cancelling`.
.. versionchanged:: 3.13
Improved handling of simultaneous internal and external cancellations
and correct preservation of cancellation counts.
Sleeping Sleeping
======== ========
@ -1369,6 +1390,15 @@ Task Object
catching :exc:`CancelledError`, it needs to call this method to remove catching :exc:`CancelledError`, it needs to call this method to remove
the cancellation state. the cancellation state.
When this method decrements the cancellation count to zero,
the method checks if a previous :meth:`cancel` call had arranged
for :exc:`CancelledError` to be thrown into the task.
If it hasn't been thrown yet, that arrangement will be
rescinded (by resetting the internal ``_must_cancel`` flag).
.. versionchanged:: 3.13
Changed to rescind pending cancellation requests upon reaching zero.
.. method:: cancelling() .. method:: cancelling()
Return the number of pending cancellation requests to this Task, i.e., Return the number of pending cancellation requests to this Task, i.e.,

View File

@ -1385,22 +1385,23 @@ These can be used as types in annotations. They all support subscription using
.. versionadded:: 3.9 .. versionadded:: 3.9
.. data:: TypeGuard .. data:: TypeIs
Special typing construct for marking user-defined type guard functions. Special typing construct for marking user-defined type predicate functions.
``TypeGuard`` can be used to annotate the return type of a user-defined ``TypeIs`` can be used to annotate the return type of a user-defined
type guard function. ``TypeGuard`` only accepts a single type argument. type predicate function. ``TypeIs`` only accepts a single type argument.
At runtime, functions marked this way should return a boolean. At runtime, functions marked this way should return a boolean and take at
least one positional argument.
``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static ``TypeIs`` aims to benefit *type narrowing* -- a technique used by static
type checkers to determine a more precise type of an expression within a type checkers to determine a more precise type of an expression within a
program's code flow. Usually type narrowing is done by analyzing program's code flow. Usually type narrowing is done by analyzing
conditional code flow and applying the narrowing to a block of code. The conditional code flow and applying the narrowing to a block of code. The
conditional expression here is sometimes referred to as a "type guard":: conditional expression here is sometimes referred to as a "type predicate"::
def is_str(val: str | float): def is_str(val: str | float):
# "isinstance" type guard # "isinstance" type predicate
if isinstance(val, str): if isinstance(val, str):
# Type of ``val`` is narrowed to ``str`` # Type of ``val`` is narrowed to ``str``
... ...
@ -1409,8 +1410,73 @@ These can be used as types in annotations. They all support subscription using
... ...
Sometimes it would be convenient to use a user-defined boolean function Sometimes it would be convenient to use a user-defined boolean function
as a type guard. Such a function should use ``TypeGuard[...]`` as its as a type predicate. Such a function should use ``TypeIs[...]`` or
return type to alert static type checkers to this intention. :data:`TypeGuard` as its return type to alert static type checkers to
this intention. ``TypeIs`` usually has more intuitive behavior than
``TypeGuard``, but it cannot be used when the input and output types
are incompatible (e.g., ``list[object]`` to ``list[int]``) or when the
function does not return ``True`` for all instances of the narrowed type.
Using ``-> TypeIs[NarrowedType]`` tells the static type checker that for a given
function:
1. The return value is a boolean.
2. If the return value is ``True``, the type of its argument
is the intersection of the argument's original type and ``NarrowedType``.
3. If the return value is ``False``, the type of its argument
is narrowed to exclude ``NarrowedType``.
For example::
from typing import assert_type, final, TypeIs
class Parent: pass
class Child(Parent): pass
@final
class Unrelated: pass
def is_parent(val: object) -> TypeIs[Parent]:
return isinstance(val, Parent)
def run(arg: Child | Unrelated):
if is_parent(arg):
# Type of ``arg`` is narrowed to the intersection
# of ``Parent`` and ``Child``, which is equivalent to
# ``Child``.
assert_type(arg, Child)
else:
# Type of ``arg`` is narrowed to exclude ``Parent``,
# so only ``Unrelated`` is left.
assert_type(arg, Unrelated)
The type inside ``TypeIs`` must be consistent with the type of the
function's argument; if it is not, static type checkers will raise
an error. An incorrectly written ``TypeIs`` function can lead to
unsound behavior in the type system; it is the user's responsibility
to write such functions in a type-safe manner.
If a ``TypeIs`` function is a class or instance method, then the type in
``TypeIs`` maps to the type of the second parameter after ``cls`` or
``self``.
In short, the form ``def foo(arg: TypeA) -> TypeIs[TypeB]: ...``,
means that if ``foo(arg)`` returns ``True``, then ``arg`` is an instance
of ``TypeB``, and if it returns ``False``, it is not an instance of ``TypeB``.
``TypeIs`` also works with type variables. For more information, see
:pep:`742` (Narrowing types with ``TypeIs``).
.. versionadded:: 3.13
.. data:: TypeGuard
Special typing construct for marking user-defined type predicate functions.
Type predicate functions are user-defined functions that return whether their
argument is an instance of a particular type.
``TypeGuard`` works similarly to :data:`TypeIs`, but has subtly different
effects on type checking behavior (see below).
Using ``-> TypeGuard`` tells the static type checker that for a given Using ``-> TypeGuard`` tells the static type checker that for a given
function: function:
@ -1419,6 +1485,8 @@ These can be used as types in annotations. They all support subscription using
2. If the return value is ``True``, the type of its argument 2. If the return value is ``True``, the type of its argument
is the type inside ``TypeGuard``. is the type inside ``TypeGuard``.
``TypeGuard`` also works with type variables. See :pep:`647` for more details.
For example:: For example::
def is_str_list(val: list[object]) -> TypeGuard[list[str]]: def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
@ -1433,23 +1501,19 @@ These can be used as types in annotations. They all support subscription using
# Type of ``val`` remains as ``list[object]``. # Type of ``val`` remains as ``list[object]``.
print("Not a list of strings!") print("Not a list of strings!")
If ``is_str_list`` is a class or instance method, then the type in ``TypeIs`` and ``TypeGuard`` differ in the following ways:
``TypeGuard`` maps to the type of the second parameter after ``cls`` or
``self``.
In short, the form ``def foo(arg: TypeA) -> TypeGuard[TypeB]: ...``, * ``TypeIs`` requires the narrowed type to be a subtype of the input type, while
means that if ``foo(arg)`` returns ``True``, then ``arg`` narrows from ``TypeGuard`` does not. The main reason is to allow for things like
``TypeA`` to ``TypeB``. narrowing ``list[object]`` to ``list[str]`` even though the latter
is not a subtype of the former, since ``list`` is invariant.
.. note:: * When a ``TypeGuard`` function returns ``True``, type checkers narrow the type of the
variable to exactly the ``TypeGuard`` type. When a ``TypeIs`` function returns ``True``,
``TypeB`` need not be a narrower form of ``TypeA`` -- it can even be a type checkers can infer a more precise type combining the previously known type of the
wider form. The main reason is to allow for things like variable with the ``TypeIs`` type. (Technically, this is known as an intersection type.)
narrowing ``list[object]`` to ``list[str]`` even though the latter * When a ``TypeGuard`` function returns ``False``, type checkers cannot narrow the type of
is not a subtype of the former, since ``list`` is invariant. the variable at all. When a ``TypeIs`` function returns ``False``, type checkers can narrow
The responsibility of writing type-safe type guards is left to the user. the type of the variable to exclude the ``TypeIs`` type.
``TypeGuard`` also works with type variables. See :pep:`647` for more details.
.. versionadded:: 3.10 .. versionadded:: 3.10

View File

@ -87,6 +87,10 @@ Interpreter improvements:
Performance improvements are modest -- we expect to be improving this Performance improvements are modest -- we expect to be improving this
over the next few releases. over the next few releases.
New typing features:
* :pep:`742`: :data:`typing.TypeIs` was added, providing more intuitive
type narrowing behavior.
New Features New Features
============ ============
@ -192,13 +196,6 @@ Other Language Changes
(Contributed by Sebastian Pipping in :gh:`115623`.) (Contributed by Sebastian Pipping in :gh:`115623`.)
* When :func:`asyncio.TaskGroup.create_task` is called on an inactive
:class:`asyncio.TaskGroup`, the given coroutine will be closed (which
prevents a :exc:`RuntimeWarning` about the given coroutine being
never awaited).
(Contributed by Arthur Tacca and Jason Zhang in :gh:`115957`.)
* The :func:`ssl.create_default_context` API now includes * The :func:`ssl.create_default_context` API now includes
:data:`ssl.VERIFY_X509_PARTIAL_CHAIN` and :data:`ssl.VERIFY_X509_STRICT` :data:`ssl.VERIFY_X509_PARTIAL_CHAIN` and :data:`ssl.VERIFY_X509_STRICT`
in its default flags. in its default flags.
@ -296,6 +293,33 @@ asyncio
with the tasks being completed. with the tasks being completed.
(Contributed by Justin Arthur in :gh:`77714`.) (Contributed by Justin Arthur in :gh:`77714`.)
* When :func:`asyncio.TaskGroup.create_task` is called on an inactive
:class:`asyncio.TaskGroup`, the given coroutine will be closed (which
prevents a :exc:`RuntimeWarning` about the given coroutine being
never awaited).
(Contributed by Arthur Tacca and Jason Zhang in :gh:`115957`.)
* Improved behavior of :class:`asyncio.TaskGroup` when an external cancellation
collides with an internal cancellation. For example, when two task groups
are nested and both experience an exception in a child task simultaneously,
it was possible that the outer task group would hang, because its internal
cancellation was swallowed by the inner task group.
In the case where a task group is cancelled externally and also must
raise an :exc:`ExceptionGroup`, it will now call the parent task's
:meth:`~asyncio.Task.cancel` method. This ensures that a
:exc:`asyncio.CancelledError` will be raised at the next
:keyword:`await`, so the cancellation is not lost.
An added benefit of these changes is that task groups now preserve the
cancellation count (:meth:`asyncio.Task.cancelling`).
In order to handle some corner cases, :meth:`asyncio.Task.uncancel` may now
reset the undocumented ``_must_cancel`` flag when the cancellation count
reaches zero.
(Inspired by an issue reported by Arthur Tacca in :gh:`116720`.)
* Add :meth:`asyncio.Queue.shutdown` (along with * Add :meth:`asyncio.Queue.shutdown` (along with
:exc:`asyncio.QueueShutDown`) for queue termination. :exc:`asyncio.QueueShutDown`) for queue termination.
(Contributed by Laurie Opperman and Yves Duprat in :gh:`104228`.) (Contributed by Laurie Opperman and Yves Duprat in :gh:`104228`.)
@ -2006,6 +2030,11 @@ Removed
(Contributed by Victor Stinner in :gh:`105182`.) (Contributed by Victor Stinner in :gh:`105182`.)
* Remove private ``_PyObject_FastCall()`` function:
use ``PyObject_Vectorcall()`` which is available since Python 3.8
(:pep:`590`).
(Contributed by Victor Stinner in :gh:`106023`.)
* Remove ``cpython/pytime.h`` header file: it only contained private functions. * Remove ``cpython/pytime.h`` header file: it only contained private functions.
(Contributed by Victor Stinner in :gh:`106316`.) (Contributed by Victor Stinner in :gh:`106316`.)

View File

@ -77,12 +77,6 @@ class TaskGroup:
propagate_cancellation_error = exc propagate_cancellation_error = exc
else: else:
propagate_cancellation_error = None propagate_cancellation_error = None
if self._parent_cancel_requested:
# If this flag is set we *must* call uncancel().
if self._parent_task.uncancel() == 0:
# If there are no pending cancellations left,
# don't propagate CancelledError.
propagate_cancellation_error = None
if et is not None: if et is not None:
if not self._aborting: if not self._aborting:
@ -130,6 +124,13 @@ class TaskGroup:
if self._base_error is not None: if self._base_error is not None:
raise self._base_error raise self._base_error
if self._parent_cancel_requested:
# If this flag is set we *must* call uncancel().
if self._parent_task.uncancel() == 0:
# If there are no pending cancellations left,
# don't propagate CancelledError.
propagate_cancellation_error = None
# Propagate CancelledError if there is one, except if there # Propagate CancelledError if there is one, except if there
# are other errors -- those have priority. # are other errors -- those have priority.
if propagate_cancellation_error is not None and not self._errors: if propagate_cancellation_error is not None and not self._errors:
@ -139,6 +140,12 @@ class TaskGroup:
self._errors.append(exc) self._errors.append(exc)
if self._errors: if self._errors:
# If the parent task is being cancelled from the outside
# of the taskgroup, un-cancel and re-cancel the parent task,
# which will keep the cancel count stable.
if self._parent_task.cancelling():
self._parent_task.uncancel()
self._parent_task.cancel()
# Exceptions are heavy objects that can have object # Exceptions are heavy objects that can have object
# cycles (bad for GC); let's not keep a reference to # cycles (bad for GC); let's not keep a reference to
# a bunch of them. # a bunch of them.

View File

@ -255,6 +255,8 @@ class Task(futures._PyFuture): # Inherit Python Task implementation
""" """
if self._num_cancels_requested > 0: if self._num_cancels_requested > 0:
self._num_cancels_requested -= 1 self._num_cancels_requested -= 1
if self._num_cancels_requested == 0:
self._must_cancel = False
return self._num_cancels_requested return self._num_cancels_requested
def __eager_start(self): def __eager_start(self):

View File

@ -1088,8 +1088,6 @@ class EnumType(type):
setattr(cls, name, member) setattr(cls, name, member)
# now add to _member_map_ (even aliases) # now add to _member_map_ (even aliases)
cls._member_map_[name] = member cls._member_map_[name] = member
#
cls._member_map_[name] = member
EnumMeta = EnumType # keep EnumMeta name for backwards compatibility EnumMeta = EnumType # keep EnumMeta name for backwards compatibility
@ -1802,20 +1800,31 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None):
for name, value in attrs.items(): for name, value in attrs.items():
if isinstance(value, auto) and auto.value is _auto_null: if isinstance(value, auto) and auto.value is _auto_null:
value = gnv(name, 1, len(member_names), gnv_last_values) value = gnv(name, 1, len(member_names), gnv_last_values)
if value in value2member_map or value in unhashable_values: # create basic member (possibly isolate value for alias check)
# an alias to an existing member if use_args:
enum_class(value)._add_alias_(name) if not isinstance(value, tuple):
value = (value, )
member = new_member(enum_class, *value)
value = value[0]
else: else:
# create the member member = new_member(enum_class)
if use_args: if __new__ is None:
if not isinstance(value, tuple): member._value_ = value
value = (value, ) # now check if alias
member = new_member(enum_class, *value) try:
value = value[0] contained = value2member_map.get(member._value_)
else: except TypeError:
member = new_member(enum_class) contained = None
if __new__ is None: if member._value_ in unhashable_values:
member._value_ = value for m in enum_class:
if m._value_ == member._value_:
contained = m
break
if contained is not None:
# an alias to an existing member
contained._add_alias_(name)
else:
# finish creating member
member._name_ = name member._name_ = name
member.__objclass__ = enum_class member.__objclass__ = enum_class
member.__init__(value) member.__init__(value)
@ -1847,24 +1856,31 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None):
if value.value is _auto_null: if value.value is _auto_null:
value.value = gnv(name, 1, len(member_names), gnv_last_values) value.value = gnv(name, 1, len(member_names), gnv_last_values)
value = value.value value = value.value
try: # create basic member (possibly isolate value for alias check)
contained = value in value2member_map if use_args:
except TypeError: if not isinstance(value, tuple):
contained = value in unhashable_values value = (value, )
if contained: member = new_member(enum_class, *value)
# an alias to an existing member value = value[0]
enum_class(value)._add_alias_(name)
else: else:
# create the member member = new_member(enum_class)
if use_args: if __new__ is None:
if not isinstance(value, tuple): member._value_ = value
value = (value, ) # now check if alias
member = new_member(enum_class, *value) try:
value = value[0] contained = value2member_map.get(member._value_)
else: except TypeError:
member = new_member(enum_class) contained = None
if __new__ is None: if member._value_ in unhashable_values:
member._value_ = value for m in enum_class:
if m._value_ == member._value_:
contained = m
break
if contained is not None:
# an alias to an existing member
contained._add_alias_(name)
else:
# finish creating member
member._name_ = name member._name_ = name
member.__objclass__ = enum_class member.__objclass__ = enum_class
member.__init__(value) member.__init__(value)

View File

@ -1927,6 +1927,10 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase):
'2009-02-29', # Invalid leap day '2009-02-29', # Invalid leap day
'2019-W53-1', # No week 53 in 2019 '2019-W53-1', # No week 53 in 2019
'2020-W54-1', # No week 54 '2020-W54-1', # No week 54
'0000-W25-1', # Invalid year
'10000-W25-1', # Invalid year
'2020-W25-0', # Invalid day-of-week
'2020-W25-8', # Invalid day-of-week
'2009\ud80002\ud80028', # Separators are surrogate codepoints '2009\ud80002\ud80028', # Separators are surrogate codepoints
] ]

View File

@ -833,6 +833,72 @@ class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.run_until_complete(run_coro_after_tg_closes()) loop.run_until_complete(run_coro_after_tg_closes())
async def test_cancelling_level_preserved(self):
async def raise_after(t, e):
await asyncio.sleep(t)
raise e()
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(raise_after(0.0, RuntimeError))
except* RuntimeError:
pass
self.assertEqual(asyncio.current_task().cancelling(), 0)
async def test_nested_groups_both_cancelled(self):
async def raise_after(t, e):
await asyncio.sleep(t)
raise e()
try:
async with asyncio.TaskGroup() as outer_tg:
try:
async with asyncio.TaskGroup() as inner_tg:
inner_tg.create_task(raise_after(0, RuntimeError))
outer_tg.create_task(raise_after(0, ValueError))
except* RuntimeError:
pass
else:
self.fail("RuntimeError not raised")
self.assertEqual(asyncio.current_task().cancelling(), 1)
except* ValueError:
pass
else:
self.fail("ValueError not raised")
self.assertEqual(asyncio.current_task().cancelling(), 0)
async def test_error_and_cancel(self):
event = asyncio.Event()
async def raise_error():
event.set()
await asyncio.sleep(0)
raise RuntimeError()
async def inner():
try:
async with taskgroups.TaskGroup() as tg:
tg.create_task(raise_error())
await asyncio.sleep(1)
self.fail("Sleep in group should have been cancelled")
except* RuntimeError:
self.assertEqual(asyncio.current_task().cancelling(), 1)
self.assertEqual(asyncio.current_task().cancelling(), 1)
await asyncio.sleep(1)
self.fail("Sleep after group should have been cancelled")
async def outer():
t = asyncio.create_task(inner())
await event.wait()
self.assertEqual(t.cancelling(), 0)
t.cancel()
self.assertEqual(t.cancelling(), 1)
with self.assertRaises(asyncio.CancelledError):
await t
self.assertTrue(t.cancelled())
await outer()
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -684,6 +684,30 @@ class BaseTaskTests:
finally: finally:
loop.close() loop.close()
def test_uncancel_resets_must_cancel(self):
async def coro():
await fut
return 42
loop = asyncio.new_event_loop()
fut = asyncio.Future(loop=loop)
task = self.new_task(loop, coro())
loop.run_until_complete(asyncio.sleep(0)) # Get task waiting for fut
fut.set_result(None) # Make task runnable
try:
task.cancel() # Enter cancelled state
self.assertEqual(task.cancelling(), 1)
self.assertTrue(task._must_cancel)
task.uncancel() # Undo cancellation
self.assertEqual(task.cancelling(), 0)
self.assertFalse(task._must_cancel)
finally:
res = loop.run_until_complete(task)
self.assertEqual(res, 42)
loop.close()
def test_cancel(self): def test_cancel(self):
def gen(): def gen():

View File

@ -18,8 +18,12 @@ import types
import contextlib import contextlib
if not support.has_subprocess_support: def doctest_skip_if(condition):
raise unittest.SkipTest("test_CLI requires subprocess support.") def decorator(func):
if condition and support.HAVE_DOCSTRINGS:
func.__doc__ = ">>> pass # doctest: +SKIP"
return func
return decorator
# NOTE: There are some additional tests relating to interaction with # NOTE: There are some additional tests relating to interaction with
@ -466,7 +470,7 @@ We'll simulate a __file__ attr that ends in pyc:
>>> tests = finder.find(sample_func) >>> tests = finder.find(sample_func)
>>> print(tests) # doctest: +ELLIPSIS >>> print(tests) # doctest: +ELLIPSIS
[<DocTest sample_func from test_doctest.py:33 (1 example)>] [<DocTest sample_func from test_doctest.py:37 (1 example)>]
The exact name depends on how test_doctest was invoked, so allow for The exact name depends on how test_doctest was invoked, so allow for
leading path components. leading path components.
@ -2966,6 +2970,7 @@ Check doctest with a non-ascii filename:
TestResults(failed=1, attempted=1) TestResults(failed=1, attempted=1)
""" """
@doctest_skip_if(not support.has_subprocess_support)
def test_CLI(): r""" def test_CLI(): r"""
The doctest module can be used to run doctests against an arbitrary file. The doctest module can be used to run doctests against an arbitrary file.
These tests test this CLI functionality. These tests test this CLI functionality.

View File

@ -5170,7 +5170,57 @@ class TestStdLib(unittest.TestCase):
self.assertIn('python', Unhashable) self.assertIn('python', Unhashable)
self.assertEqual(Unhashable.name.value, 'python') self.assertEqual(Unhashable.name.value, 'python')
self.assertEqual(Unhashable.name.name, 'name') self.assertEqual(Unhashable.name.name, 'name')
_test_simple_enum(Unhashable, Unhashable) _test_simple_enum(CheckedUnhashable, Unhashable)
##
class CheckedComplexStatus(IntEnum):
def __new__(cls, value, phrase, description=''):
obj = int.__new__(cls, value)
obj._value_ = value
obj.phrase = phrase
obj.description = description
return obj
CONTINUE = 100, 'Continue', 'Request received, please continue'
PROCESSING = 102, 'Processing'
EARLY_HINTS = 103, 'Early Hints'
SOME_HINTS = 103, 'Some Early Hints'
#
@_simple_enum(IntEnum)
class ComplexStatus:
def __new__(cls, value, phrase, description=''):
obj = int.__new__(cls, value)
obj._value_ = value
obj.phrase = phrase
obj.description = description
return obj
CONTINUE = 100, 'Continue', 'Request received, please continue'
PROCESSING = 102, 'Processing'
EARLY_HINTS = 103, 'Early Hints'
SOME_HINTS = 103, 'Some Early Hints'
_test_simple_enum(CheckedComplexStatus, ComplexStatus)
#
#
class CheckedComplexFlag(IntFlag):
def __new__(cls, value, label):
obj = int.__new__(cls, value)
obj._value_ = value
obj.label = label
return obj
SHIRT = 1, 'upper half'
VEST = 1, 'outer upper half'
PANTS = 2, 'lower half'
self.assertIs(CheckedComplexFlag.SHIRT, CheckedComplexFlag.VEST)
#
@_simple_enum(IntFlag)
class ComplexFlag:
def __new__(cls, value, label):
obj = int.__new__(cls, value)
obj._value_ = value
obj.label = label
return obj
SHIRT = 1, 'upper half'
VEST = 1, 'uppert half'
PANTS = 2, 'lower half'
_test_simple_enum(CheckedComplexFlag, ComplexFlag)
class MiscTestCase(unittest.TestCase): class MiscTestCase(unittest.TestCase):

View File

@ -575,10 +575,12 @@ class FaultHandlerTests(unittest.TestCase):
lineno = 8 lineno = 8
else: else:
lineno = 10 lineno = 10
# When the traceback is dumped, the waiter thread may be in the
# `self.running.set()` call or in `self.stop.wait()`.
regex = r""" regex = r"""
^Thread 0x[0-9a-f]+ \(most recent call first\): ^Thread 0x[0-9a-f]+ \(most recent call first\):
(?: File ".*threading.py", line [0-9]+ in [_a-z]+ (?: File ".*threading.py", line [0-9]+ in [_a-z]+
){{1,3}} File "<string>", line 23 in run ){{1,3}} File "<string>", line (?:22|23) in run
File ".*threading.py", line [0-9]+ in _bootstrap_inner File ".*threading.py", line [0-9]+ in _bootstrap_inner
File ".*threading.py", line [0-9]+ in _bootstrap File ".*threading.py", line [0-9]+ in _bootstrap

View File

@ -38,7 +38,7 @@ from typing import Annotated, ForwardRef
from typing import Self, LiteralString from typing import Self, LiteralString
from typing import TypeAlias from typing import TypeAlias
from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs
from typing import TypeGuard from typing import TypeGuard, TypeIs
import abc import abc
import textwrap import textwrap
import typing import typing
@ -5207,6 +5207,7 @@ class GenericTests(BaseTestCase):
Literal[1, 2], Literal[1, 2],
Concatenate[int, ParamSpec("P")], Concatenate[int, ParamSpec("P")],
TypeGuard[int], TypeGuard[int],
TypeIs[range],
): ):
with self.subTest(msg=obj): with self.subTest(msg=obj):
with self.assertRaisesRegex( with self.assertRaisesRegex(
@ -6748,6 +6749,7 @@ class GetUtilitiesTestCase(TestCase):
self.assertEqual(get_args(NotRequired[int]), (int,)) self.assertEqual(get_args(NotRequired[int]), (int,))
self.assertEqual(get_args(TypeAlias), ()) self.assertEqual(get_args(TypeAlias), ())
self.assertEqual(get_args(TypeGuard[int]), (int,)) self.assertEqual(get_args(TypeGuard[int]), (int,))
self.assertEqual(get_args(TypeIs[range]), (range,))
Ts = TypeVarTuple('Ts') Ts = TypeVarTuple('Ts')
self.assertEqual(get_args(Ts), ()) self.assertEqual(get_args(Ts), ())
self.assertEqual(get_args((*Ts,)[0]), (Ts,)) self.assertEqual(get_args((*Ts,)[0]), (Ts,))
@ -9592,6 +9594,56 @@ class TypeGuardTests(BaseTestCase):
issubclass(int, TypeGuard) issubclass(int, TypeGuard)
class TypeIsTests(BaseTestCase):
def test_basics(self):
TypeIs[int] # OK
def foo(arg) -> TypeIs[int]: ...
self.assertEqual(gth(foo), {'return': TypeIs[int]})
with self.assertRaises(TypeError):
TypeIs[int, str]
def test_repr(self):
self.assertEqual(repr(TypeIs), 'typing.TypeIs')
cv = TypeIs[int]
self.assertEqual(repr(cv), 'typing.TypeIs[int]')
cv = TypeIs[Employee]
self.assertEqual(repr(cv), 'typing.TypeIs[%s.Employee]' % __name__)
cv = TypeIs[tuple[int]]
self.assertEqual(repr(cv), 'typing.TypeIs[tuple[int]]')
def test_cannot_subclass(self):
with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE):
class C(type(TypeIs)):
pass
with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE):
class D(type(TypeIs[int])):
pass
with self.assertRaisesRegex(TypeError,
r'Cannot subclass typing\.TypeIs'):
class E(TypeIs):
pass
with self.assertRaisesRegex(TypeError,
r'Cannot subclass typing\.TypeIs\[int\]'):
class F(TypeIs[int]):
pass
def test_cannot_init(self):
with self.assertRaises(TypeError):
TypeIs()
with self.assertRaises(TypeError):
type(TypeIs)()
with self.assertRaises(TypeError):
type(TypeIs[Optional[int]])()
def test_no_isinstance(self):
with self.assertRaises(TypeError):
isinstance(1, TypeIs[int])
with self.assertRaises(TypeError):
issubclass(int, TypeIs)
SpecialAttrsP = typing.ParamSpec('SpecialAttrsP') SpecialAttrsP = typing.ParamSpec('SpecialAttrsP')
SpecialAttrsT = typing.TypeVar('SpecialAttrsT', int, float, complex) SpecialAttrsT = typing.TypeVar('SpecialAttrsT', int, float, complex)
@ -9691,6 +9743,7 @@ class SpecialAttrsTests(BaseTestCase):
typing.Optional: 'Optional', typing.Optional: 'Optional',
typing.TypeAlias: 'TypeAlias', typing.TypeAlias: 'TypeAlias',
typing.TypeGuard: 'TypeGuard', typing.TypeGuard: 'TypeGuard',
typing.TypeIs: 'TypeIs',
typing.TypeVar: 'TypeVar', typing.TypeVar: 'TypeVar',
typing.Union: 'Union', typing.Union: 'Union',
typing.Self: 'Self', typing.Self: 'Self',
@ -9705,6 +9758,7 @@ class SpecialAttrsTests(BaseTestCase):
typing.Literal[True, 2]: 'Literal', typing.Literal[True, 2]: 'Literal',
typing.Optional[Any]: 'Optional', typing.Optional[Any]: 'Optional',
typing.TypeGuard[Any]: 'TypeGuard', typing.TypeGuard[Any]: 'TypeGuard',
typing.TypeIs[Any]: 'TypeIs',
typing.Union[Any]: 'Any', typing.Union[Any]: 'Any',
typing.Union[int, float]: 'Union', typing.Union[int, float]: 'Union',
# Incompatible special forms (tested in test_special_attrs2) # Incompatible special forms (tested in test_special_attrs2)

View File

@ -153,6 +153,7 @@ __all__ = [
'TYPE_CHECKING', 'TYPE_CHECKING',
'TypeAlias', 'TypeAlias',
'TypeGuard', 'TypeGuard',
'TypeIs',
'TypeAliasType', 'TypeAliasType',
'Unpack', 'Unpack',
] ]
@ -818,28 +819,31 @@ def Concatenate(self, parameters):
@_SpecialForm @_SpecialForm
def TypeGuard(self, parameters): def TypeGuard(self, parameters):
"""Special typing construct for marking user-defined type guard functions. """Special typing construct for marking user-defined type predicate functions.
``TypeGuard`` can be used to annotate the return type of a user-defined ``TypeGuard`` can be used to annotate the return type of a user-defined
type guard function. ``TypeGuard`` only accepts a single type argument. type predicate function. ``TypeGuard`` only accepts a single type argument.
At runtime, functions marked this way should return a boolean. At runtime, functions marked this way should return a boolean.
``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static
type checkers to determine a more precise type of an expression within a type checkers to determine a more precise type of an expression within a
program's code flow. Usually type narrowing is done by analyzing program's code flow. Usually type narrowing is done by analyzing
conditional code flow and applying the narrowing to a block of code. The conditional code flow and applying the narrowing to a block of code. The
conditional expression here is sometimes referred to as a "type guard". conditional expression here is sometimes referred to as a "type predicate".
Sometimes it would be convenient to use a user-defined boolean function Sometimes it would be convenient to use a user-defined boolean function
as a type guard. Such a function should use ``TypeGuard[...]`` as its as a type predicate. Such a function should use ``TypeGuard[...]`` or
return type to alert static type checkers to this intention. ``TypeIs[...]`` as its return type to alert static type checkers to
this intention. ``TypeGuard`` should be used over ``TypeIs`` when narrowing
from an incompatible type (e.g., ``list[object]`` to ``list[int]``) or when
the function does not return ``True`` for all instances of the narrowed type.
Using ``-> TypeGuard`` tells the static type checker that for a given Using ``-> TypeGuard[NarrowedType]`` tells the static type checker that
function: for a given function:
1. The return value is a boolean. 1. The return value is a boolean.
2. If the return value is ``True``, the type of its argument 2. If the return value is ``True``, the type of its argument
is the type inside ``TypeGuard``. is ``NarrowedType``.
For example:: For example::
@ -860,7 +864,7 @@ def TypeGuard(self, parameters):
type-unsafe results. The main reason is to allow for things like type-unsafe results. The main reason is to allow for things like
narrowing ``list[object]`` to ``list[str]`` even though the latter is not narrowing ``list[object]`` to ``list[str]`` even though the latter is not
a subtype of the former, since ``list`` is invariant. The responsibility of a subtype of the former, since ``list`` is invariant. The responsibility of
writing type-safe type guards is left to the user. writing type-safe type predicates is left to the user.
``TypeGuard`` also works with type variables. For more information, see ``TypeGuard`` also works with type variables. For more information, see
PEP 647 (User-Defined Type Guards). PEP 647 (User-Defined Type Guards).
@ -869,6 +873,75 @@ def TypeGuard(self, parameters):
return _GenericAlias(self, (item,)) return _GenericAlias(self, (item,))
@_SpecialForm
def TypeIs(self, parameters):
"""Special typing construct for marking user-defined type predicate functions.
``TypeIs`` can be used to annotate the return type of a user-defined
type predicate function. ``TypeIs`` only accepts a single type argument.
At runtime, functions marked this way should return a boolean and accept
at least one argument.
``TypeIs`` aims to benefit *type narrowing* -- a technique used by static
type checkers to determine a more precise type of an expression within a
program's code flow. Usually type narrowing is done by analyzing
conditional code flow and applying the narrowing to a block of code. The
conditional expression here is sometimes referred to as a "type predicate".
Sometimes it would be convenient to use a user-defined boolean function
as a type predicate. Such a function should use ``TypeIs[...]`` or
``TypeGuard[...]`` as its return type to alert static type checkers to
this intention. ``TypeIs`` usually has more intuitive behavior than
``TypeGuard``, but it cannot be used when the input and output types
are incompatible (e.g., ``list[object]`` to ``list[int]``) or when the
function does not return ``True`` for all instances of the narrowed type.
Using ``-> TypeIs[NarrowedType]`` tells the static type checker that for
a given function:
1. The return value is a boolean.
2. If the return value is ``True``, the type of its argument
is the intersection of the argument's original type and
``NarrowedType``.
3. If the return value is ``False``, the type of its argument
is narrowed to exclude ``NarrowedType``.
For example::
from typing import assert_type, final, TypeIs
class Parent: pass
class Child(Parent): pass
@final
class Unrelated: pass
def is_parent(val: object) -> TypeIs[Parent]:
return isinstance(val, Parent)
def run(arg: Child | Unrelated):
if is_parent(arg):
# Type of ``arg`` is narrowed to the intersection
# of ``Parent`` and ``Child``, which is equivalent to
# ``Child``.
assert_type(arg, Child)
else:
# Type of ``arg`` is narrowed to exclude ``Parent``,
# so only ``Unrelated`` is left.
assert_type(arg, Unrelated)
The type inside ``TypeIs`` must be consistent with the type of the
function's argument; if it is not, static type checkers will raise
an error. An incorrectly written ``TypeIs`` function can lead to
unsound behavior in the type system; it is the user's responsibility
to write such functions in a type-safe manner.
``TypeIs`` also works with type variables. For more information, see
PEP 742 (Narrowing types with ``TypeIs``).
"""
item = _type_check(parameters, f'{self} accepts only single type.')
return _GenericAlias(self, (item,))
class ForwardRef(_Final, _root=True): class ForwardRef(_Final, _root=True):
"""Internal wrapper to hold a forward reference.""" """Internal wrapper to hold a forward reference."""
@ -1241,11 +1314,12 @@ class _GenericAlias(_BaseGenericAlias, _root=True):
# A = Callable[[], None] # _CallableGenericAlias # A = Callable[[], None] # _CallableGenericAlias
# B = Callable[[T], None] # _CallableGenericAlias # B = Callable[[T], None] # _CallableGenericAlias
# C = B[int] # _CallableGenericAlias # C = B[int] # _CallableGenericAlias
# * Parameterized `Final`, `ClassVar` and `TypeGuard`: # * Parameterized `Final`, `ClassVar`, `TypeGuard`, and `TypeIs`:
# # All _GenericAlias # # All _GenericAlias
# Final[int] # Final[int]
# ClassVar[float] # ClassVar[float]
# TypeVar[bool] # TypeGuard[bool]
# TypeIs[range]
def __init__(self, origin, args, *, inst=True, name=None): def __init__(self, origin, args, *, inst=True, name=None):
super().__init__(origin, inst=inst, name=name) super().__init__(origin, inst=inst, name=name)

View File

@ -496,6 +496,7 @@ David Edelsohn
John Edmonds John Edmonds
Benjamin Edwards Benjamin Edwards
Grant Edwards Grant Edwards
Vlad Efanov
Zvi Effron Zvi Effron
John Ehresman John Ehresman
Tal Einat Tal Einat

View File

@ -0,0 +1,2 @@
Improve validation logic in the C implementation of :meth:`datetime.fromisoformat`
to better handle invalid years. Patch by Vlad Efanov.

View File

@ -0,0 +1 @@
Add :data:`typing.TypeIs`, implementing :pep:`742`. Patch by Jelle Zijlstra.

View File

@ -0,0 +1,18 @@
Improved behavior of :class:`asyncio.TaskGroup` when an external cancellation
collides with an internal cancellation. For example, when two task groups
are nested and both experience an exception in a child task simultaneously,
it was possible that the outer task group would misbehave, because
its internal cancellation was swallowed by the inner task group.
In the case where a task group is cancelled externally and also must
raise an :exc:`ExceptionGroup`, it will now call the parent task's
:meth:`~asyncio.Task.cancel` method. This ensures that a
:exc:`asyncio.CancelledError` will be raised at the next
:keyword:`await`, so the cancellation is not lost.
An added benefit of these changes is that task groups now preserve the
cancellation count (:meth:`asyncio.Task.cancelling`).
In order to handle some corner cases, :meth:`asyncio.Task.uncancel` may now
reset the undocumented ``_must_cancel`` flag when the cancellation count
reaches zero.

View File

@ -0,0 +1,2 @@
Fix ``_simple_enum`` to detect aliases when multiple arguments are present
but only one is the member value.

View File

@ -2393,6 +2393,9 @@ _asyncio_Task_uncancel_impl(TaskObj *self)
{ {
if (self->task_num_cancels_requested > 0) { if (self->task_num_cancels_requested > 0) {
self->task_num_cancels_requested -= 1; self->task_num_cancels_requested -= 1;
if (self->task_num_cancels_requested == 0) {
self->task_must_cancel = 0;
}
} }
return PyLong_FromLong(self->task_num_cancels_requested); return PyLong_FromLong(self->task_num_cancels_requested);
} }

View File

@ -416,6 +416,10 @@ iso_week1_monday(int year)
static int static int
iso_to_ymd(const int iso_year, const int iso_week, const int iso_day, iso_to_ymd(const int iso_year, const int iso_week, const int iso_day,
int *year, int *month, int *day) { int *year, int *month, int *day) {
// Year is bounded to 0 < year < 10000 because 9999-12-31 is (9999, 52, 5)
if (iso_year < MINYEAR || iso_year > MAXYEAR) {
return -4;
}
if (iso_week <= 0 || iso_week >= 53) { if (iso_week <= 0 || iso_week >= 53) {
int out_of_range = 1; int out_of_range = 1;
if (iso_week == 53) { if (iso_week == 53) {
@ -762,7 +766,7 @@ parse_isoformat_date(const char *dtstr, const size_t len, int *year, int *month,
* -2: Inconsistent date separator usage * -2: Inconsistent date separator usage
* -3: Failed to parse ISO week. * -3: Failed to parse ISO week.
* -4: Failed to parse ISO day. * -4: Failed to parse ISO day.
* -5, -6: Failure in iso_to_ymd * -5, -6, -7: Failure in iso_to_ymd
*/ */
const char *p = dtstr; const char *p = dtstr;
p = parse_digits(p, year, 4); p = parse_digits(p, year, 4);
@ -3142,15 +3146,13 @@ date_fromisocalendar(PyObject *cls, PyObject *args, PyObject *kw)
return NULL; return NULL;
} }
// Year is bounded to 0 < year < 10000 because 9999-12-31 is (9999, 52, 5)
if (year < MINYEAR || year > MAXYEAR) {
PyErr_Format(PyExc_ValueError, "Year is out of range: %d", year);
return NULL;
}
int month; int month;
int rv = iso_to_ymd(year, week, day, &year, &month, &day); int rv = iso_to_ymd(year, week, day, &year, &month, &day);
if (rv == -4) {
PyErr_Format(PyExc_ValueError, "Year is out of range: %d", year);
return NULL;
}
if (rv == -2) { if (rv == -2) {
PyErr_Format(PyExc_ValueError, "Invalid week: %d", week); PyErr_Format(PyExc_ValueError, "Invalid week: %d", week);