summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/asyncio-task.rst30
-rw-r--r--Doc/library/typing.rst118
-rw-r--r--Doc/whatsnew/3.13.rst43
-rw-r--r--Lib/asyncio/taskgroups.py19
-rw-r--r--Lib/asyncio/tasks.py2
-rw-r--r--Lib/enum.py72
-rw-r--r--Lib/test/datetimetester.py4
-rw-r--r--Lib/test/test_asyncio/test_taskgroups.py66
-rw-r--r--Lib/test/test_asyncio/test_tasks.py24
-rw-r--r--Lib/test/test_doctest/test_doctest.py11
-rw-r--r--Lib/test/test_enum.py52
-rw-r--r--Lib/test/test_faulthandler.py4
-rw-r--r--Lib/test/test_typing.py56
-rw-r--r--Lib/typing.py96
-rw-r--r--Misc/ACKS1
-rw-r--r--Misc/NEWS.d/next/C API/2024-04-08-09-44-29.gh-issue-117534.54ZE_n.rst2
-rw-r--r--Misc/NEWS.d/next/Library/2024-04-03-16-01-31.gh-issue-117516.7DlHje.rst1
-rw-r--r--Misc/NEWS.d/next/Library/2024-04-04-15-28-12.gh-issue-116720.aGhXns.rst18
-rw-r--r--Misc/NEWS.d/next/Library/2024-04-08-19-12-26.gh-issue-117663.CPfc_p.rst2
-rw-r--r--Modules/_asynciomodule.c3
-rw-r--r--Modules/_datetimemodule.c16
21 files changed, 548 insertions, 92 deletions
diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst
index 3b10a0d..3d300c3 100644
--- a/Doc/library/asyncio-task.rst
+++ b/Doc/library/asyncio-task.rst
@@ -392,6 +392,27 @@ is also included in the exception group.
The same special case is made for
: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
========
@@ -1369,6 +1390,15 @@ Task Object
catching :exc:`CancelledError`, it needs to call this method to remove
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()
Return the number of pending cancellation requests to this Task, i.e.,
diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst
index 73214e1..19dbd37 100644
--- a/Doc/library/typing.rst
+++ b/Doc/library/typing.rst
@@ -1385,22 +1385,23 @@ These can be used as types in annotations. They all support subscription using
.. 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
- type guard function. ``TypeGuard`` only accepts a single type argument.
- At runtime, functions marked this way should return a boolean.
+ ``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 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
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 guard"::
+ conditional expression here is sometimes referred to as a "type predicate"::
def is_str(val: str | float):
- # "isinstance" type guard
+ # "isinstance" type predicate
if isinstance(val, 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
- as a type guard. Such a function should use ``TypeGuard[...]`` as its
- return type to alert static type checkers to this intention.
+ as a type predicate. Such a function should use ``TypeIs[...]`` or
+ :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
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
is the type inside ``TypeGuard``.
+ ``TypeGuard`` also works with type variables. See :pep:`647` for more details.
+
For example::
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]``.
print("Not a list of strings!")
- If ``is_str_list`` is a class or instance method, then the type in
- ``TypeGuard`` maps to the type of the second parameter after ``cls`` or
- ``self``.
-
- In short, the form ``def foo(arg: TypeA) -> TypeGuard[TypeB]: ...``,
- means that if ``foo(arg)`` returns ``True``, then ``arg`` narrows from
- ``TypeA`` to ``TypeB``.
-
- .. note::
-
- ``TypeB`` need not be a narrower form of ``TypeA`` -- it can even be a
- wider form. The main reason is to allow for things like
- narrowing ``list[object]`` to ``list[str]`` even though the latter
- is not a subtype of the former, since ``list`` is invariant.
- The responsibility of writing type-safe type guards is left to the user.
-
- ``TypeGuard`` also works with type variables. See :pep:`647` for more details.
+ ``TypeIs`` and ``TypeGuard`` differ in the following ways:
+
+ * ``TypeIs`` requires the narrowed type to be a subtype of the input type, while
+ ``TypeGuard`` does not. The main reason is to allow for things like
+ narrowing ``list[object]`` to ``list[str]`` even though the latter
+ is not a subtype of the former, since ``list`` is invariant.
+ * 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``,
+ type checkers can infer a more precise type combining the previously known type of the
+ variable with the ``TypeIs`` type. (Technically, this is known as an intersection type.)
+ * When a ``TypeGuard`` function returns ``False``, type checkers cannot narrow the type of
+ the variable at all. When a ``TypeIs`` function returns ``False``, type checkers can narrow
+ the type of the variable to exclude the ``TypeIs`` type.
.. versionadded:: 3.10
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index 72b3a4c..d394fbe 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -87,6 +87,10 @@ Interpreter improvements:
Performance improvements are modest -- we expect to be improving this
over the next few releases.
+New typing features:
+
+* :pep:`742`: :data:`typing.TypeIs` was added, providing more intuitive
+ type narrowing behavior.
New Features
============
@@ -192,13 +196,6 @@ Other Language Changes
(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
:data:`ssl.VERIFY_X509_PARTIAL_CHAIN` and :data:`ssl.VERIFY_X509_STRICT`
in its default flags.
@@ -296,6 +293,33 @@ asyncio
with the tasks being completed.
(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
:exc:`asyncio.QueueShutDown`) for queue termination.
(Contributed by Laurie Opperman and Yves Duprat in :gh:`104228`.)
@@ -2006,6 +2030,11 @@ Removed
(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.
(Contributed by Victor Stinner in :gh:`106316`.)
diff --git a/Lib/asyncio/taskgroups.py b/Lib/asyncio/taskgroups.py
index 57f0123..f2ee964 100644
--- a/Lib/asyncio/taskgroups.py
+++ b/Lib/asyncio/taskgroups.py
@@ -77,12 +77,6 @@ class TaskGroup:
propagate_cancellation_error = exc
else:
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 not self._aborting:
@@ -130,6 +124,13 @@ class TaskGroup:
if self._base_error is not None:
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
# are other errors -- those have priority.
if propagate_cancellation_error is not None and not self._errors:
@@ -139,6 +140,12 @@ class TaskGroup:
self._errors.append(exc)
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
# cycles (bad for GC); let's not keep a reference to
# a bunch of them.
diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py
index 7fb697b9..dadcb5b 100644
--- a/Lib/asyncio/tasks.py
+++ b/Lib/asyncio/tasks.py
@@ -255,6 +255,8 @@ class Task(futures._PyFuture): # Inherit Python Task implementation
"""
if self._num_cancels_requested > 0:
self._num_cancels_requested -= 1
+ if self._num_cancels_requested == 0:
+ self._must_cancel = False
return self._num_cancels_requested
def __eager_start(self):
diff --git a/Lib/enum.py b/Lib/enum.py
index 2a135e1..98a49ea 100644
--- a/Lib/enum.py
+++ b/Lib/enum.py
@@ -1088,8 +1088,6 @@ class EnumType(type):
setattr(cls, name, member)
# now add to _member_map_ (even aliases)
cls._member_map_[name] = member
- #
- cls._member_map_[name] = member
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():
if isinstance(value, auto) and auto.value is _auto_null:
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)
+ if use_args:
+ if not isinstance(value, tuple):
+ value = (value, )
+ member = new_member(enum_class, *value)
+ value = value[0]
+ else:
+ member = new_member(enum_class)
+ if __new__ is None:
+ member._value_ = value
+ # now check if alias
+ try:
+ contained = value2member_map.get(member._value_)
+ except TypeError:
+ contained = None
+ if member._value_ in unhashable_values:
+ for m in enum_class:
+ if m._value_ == member._value_:
+ contained = m
+ break
+ if contained is not None:
# an alias to an existing member
- enum_class(value)._add_alias_(name)
+ contained._add_alias_(name)
else:
- # create the member
- if use_args:
- if not isinstance(value, tuple):
- value = (value, )
- member = new_member(enum_class, *value)
- value = value[0]
- else:
- member = new_member(enum_class)
- if __new__ is None:
- member._value_ = value
+ # finish creating member
member._name_ = name
member.__objclass__ = enum_class
member.__init__(value)
@@ -1847,24 +1856,31 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None):
if value.value is _auto_null:
value.value = gnv(name, 1, len(member_names), gnv_last_values)
value = value.value
+ # create basic member (possibly isolate value for alias check)
+ if use_args:
+ if not isinstance(value, tuple):
+ value = (value, )
+ member = new_member(enum_class, *value)
+ value = value[0]
+ else:
+ member = new_member(enum_class)
+ if __new__ is None:
+ member._value_ = value
+ # now check if alias
try:
- contained = value in value2member_map
+ contained = value2member_map.get(member._value_)
except TypeError:
- contained = value in unhashable_values
- if contained:
+ contained = None
+ if member._value_ in unhashable_values:
+ for m in enum_class:
+ if m._value_ == member._value_:
+ contained = m
+ break
+ if contained is not None:
# an alias to an existing member
- enum_class(value)._add_alias_(name)
+ contained._add_alias_(name)
else:
- # create the member
- if use_args:
- if not isinstance(value, tuple):
- value = (value, )
- member = new_member(enum_class, *value)
- value = value[0]
- else:
- member = new_member(enum_class)
- if __new__ is None:
- member._value_ = value
+ # finish creating member
member._name_ = name
member.__objclass__ = enum_class
member.__init__(value)
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index c772639..5701108 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -1927,6 +1927,10 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase):
'2009-02-29', # Invalid leap day
'2019-W53-1', # No week 53 in 2019
'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
]
diff --git a/Lib/test/test_asyncio/test_taskgroups.py b/Lib/test/test_asyncio/test_taskgroups.py
index 1ec8116..4852536 100644
--- a/Lib/test/test_asyncio/test_taskgroups.py
+++ b/Lib/test/test_asyncio/test_taskgroups.py
@@ -833,6 +833,72 @@ class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
loop = asyncio.get_event_loop()
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__":
unittest.main()
diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py
index bc6d88e..5b09c81 100644
--- a/Lib/test/test_asyncio/test_tasks.py
+++ b/Lib/test/test_asyncio/test_tasks.py
@@ -684,6 +684,30 @@ class BaseTaskTests:
finally:
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 gen():
diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py
index dd8cc9b..0a2a016 100644
--- a/Lib/test/test_doctest/test_doctest.py
+++ b/Lib/test/test_doctest/test_doctest.py
@@ -18,8 +18,12 @@ import types
import contextlib
-if not support.has_subprocess_support:
- raise unittest.SkipTest("test_CLI requires subprocess support.")
+def doctest_skip_if(condition):
+ 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
@@ -466,7 +470,7 @@ We'll simulate a __file__ attr that ends in pyc:
>>> tests = finder.find(sample_func)
>>> 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
leading path components.
@@ -2966,6 +2970,7 @@ Check doctest with a non-ascii filename:
TestResults(failed=1, attempted=1)
"""
+@doctest_skip_if(not support.has_subprocess_support)
def test_CLI(): r"""
The doctest module can be used to run doctests against an arbitrary file.
These tests test this CLI functionality.
diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py
index 6418d24..529dfc6 100644
--- a/Lib/test/test_enum.py
+++ b/Lib/test/test_enum.py
@@ -5170,7 +5170,57 @@ class TestStdLib(unittest.TestCase):
self.assertIn('python', Unhashable)
self.assertEqual(Unhashable.name.value, 'python')
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):
diff --git a/Lib/test/test_faulthandler.py b/Lib/test/test_faulthandler.py
index 200f34d..61ec8fe 100644
--- a/Lib/test/test_faulthandler.py
+++ b/Lib/test/test_faulthandler.py
@@ -575,10 +575,12 @@ class FaultHandlerTests(unittest.TestCase):
lineno = 8
else:
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"""
^Thread 0x[0-9a-f]+ \(most recent call first\):
(?: 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
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index 927f74e..bae0a84 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -38,7 +38,7 @@ from typing import Annotated, ForwardRef
from typing import Self, LiteralString
from typing import TypeAlias
from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs
-from typing import TypeGuard
+from typing import TypeGuard, TypeIs
import abc
import textwrap
import typing
@@ -5207,6 +5207,7 @@ class GenericTests(BaseTestCase):
Literal[1, 2],
Concatenate[int, ParamSpec("P")],
TypeGuard[int],
+ TypeIs[range],
):
with self.subTest(msg=obj):
with self.assertRaisesRegex(
@@ -6748,6 +6749,7 @@ class GetUtilitiesTestCase(TestCase):
self.assertEqual(get_args(NotRequired[int]), (int,))
self.assertEqual(get_args(TypeAlias), ())
self.assertEqual(get_args(TypeGuard[int]), (int,))
+ self.assertEqual(get_args(TypeIs[range]), (range,))
Ts = TypeVarTuple('Ts')
self.assertEqual(get_args(Ts), ())
self.assertEqual(get_args((*Ts,)[0]), (Ts,))
@@ -9592,6 +9594,56 @@ class TypeGuardTests(BaseTestCase):
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')
SpecialAttrsT = typing.TypeVar('SpecialAttrsT', int, float, complex)
@@ -9691,6 +9743,7 @@ class SpecialAttrsTests(BaseTestCase):
typing.Optional: 'Optional',
typing.TypeAlias: 'TypeAlias',
typing.TypeGuard: 'TypeGuard',
+ typing.TypeIs: 'TypeIs',
typing.TypeVar: 'TypeVar',
typing.Union: 'Union',
typing.Self: 'Self',
@@ -9705,6 +9758,7 @@ class SpecialAttrsTests(BaseTestCase):
typing.Literal[True, 2]: 'Literal',
typing.Optional[Any]: 'Optional',
typing.TypeGuard[Any]: 'TypeGuard',
+ typing.TypeIs[Any]: 'TypeIs',
typing.Union[Any]: 'Any',
typing.Union[int, float]: 'Union',
# Incompatible special forms (tested in test_special_attrs2)
diff --git a/Lib/typing.py b/Lib/typing.py
index d8e4ee3..231492c 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -153,6 +153,7 @@ __all__ = [
'TYPE_CHECKING',
'TypeAlias',
'TypeGuard',
+ 'TypeIs',
'TypeAliasType',
'Unpack',
]
@@ -818,28 +819,31 @@ def Concatenate(self, parameters):
@_SpecialForm
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
- 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.
``TypeGuard`` 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 guard".
+ 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 guard. Such a function should use ``TypeGuard[...]`` as its
- return type to alert static type checkers to this intention.
+ as a type predicate. Such a function should use ``TypeGuard[...]`` or
+ ``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
- function:
+ Using ``-> TypeGuard[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 type inside ``TypeGuard``.
+ is ``NarrowedType``.
For example::
@@ -860,7 +864,7 @@ def TypeGuard(self, parameters):
type-unsafe results. The main reason is to allow for things like
narrowing ``list[object]`` to ``list[str]`` even though the latter is not
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
PEP 647 (User-Defined Type Guards).
@@ -869,6 +873,75 @@ def TypeGuard(self, parameters):
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):
"""Internal wrapper to hold a forward reference."""
@@ -1241,11 +1314,12 @@ class _GenericAlias(_BaseGenericAlias, _root=True):
# A = Callable[[], None] # _CallableGenericAlias
# B = Callable[[T], None] # _CallableGenericAlias
# C = B[int] # _CallableGenericAlias
- # * Parameterized `Final`, `ClassVar` and `TypeGuard`:
+ # * Parameterized `Final`, `ClassVar`, `TypeGuard`, and `TypeIs`:
# # All _GenericAlias
# Final[int]
# ClassVar[float]
- # TypeVar[bool]
+ # TypeGuard[bool]
+ # TypeIs[range]
def __init__(self, origin, args, *, inst=True, name=None):
super().__init__(origin, inst=inst, name=name)
diff --git a/Misc/ACKS b/Misc/ACKS
index fe014a3..a108ec3 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -496,6 +496,7 @@ David Edelsohn
John Edmonds
Benjamin Edwards
Grant Edwards
+Vlad Efanov
Zvi Effron
John Ehresman
Tal Einat
diff --git a/Misc/NEWS.d/next/C API/2024-04-08-09-44-29.gh-issue-117534.54ZE_n.rst b/Misc/NEWS.d/next/C API/2024-04-08-09-44-29.gh-issue-117534.54ZE_n.rst
new file mode 100644
index 0000000..4b7dda6
--- /dev/null
+++ b/Misc/NEWS.d/next/C API/2024-04-08-09-44-29.gh-issue-117534.54ZE_n.rst
@@ -0,0 +1,2 @@
+Improve validation logic in the C implementation of :meth:`datetime.fromisoformat`
+to better handle invalid years. Patch by Vlad Efanov.
diff --git a/Misc/NEWS.d/next/Library/2024-04-03-16-01-31.gh-issue-117516.7DlHje.rst b/Misc/NEWS.d/next/Library/2024-04-03-16-01-31.gh-issue-117516.7DlHje.rst
new file mode 100644
index 0000000..bbf6912
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-04-03-16-01-31.gh-issue-117516.7DlHje.rst
@@ -0,0 +1 @@
+Add :data:`typing.TypeIs`, implementing :pep:`742`. Patch by Jelle Zijlstra.
diff --git a/Misc/NEWS.d/next/Library/2024-04-04-15-28-12.gh-issue-116720.aGhXns.rst b/Misc/NEWS.d/next/Library/2024-04-04-15-28-12.gh-issue-116720.aGhXns.rst
new file mode 100644
index 0000000..39c7d6b
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-04-04-15-28-12.gh-issue-116720.aGhXns.rst
@@ -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.
diff --git a/Misc/NEWS.d/next/Library/2024-04-08-19-12-26.gh-issue-117663.CPfc_p.rst b/Misc/NEWS.d/next/Library/2024-04-08-19-12-26.gh-issue-117663.CPfc_p.rst
new file mode 100644
index 0000000..2c7a522
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-04-08-19-12-26.gh-issue-117663.CPfc_p.rst
@@ -0,0 +1,2 @@
+Fix ``_simple_enum`` to detect aliases when multiple arguments are present
+but only one is the member value.
diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c
index 29246cf..b886051 100644
--- a/Modules/_asynciomodule.c
+++ b/Modules/_asynciomodule.c
@@ -2393,6 +2393,9 @@ _asyncio_Task_uncancel_impl(TaskObj *self)
{
if (self->task_num_cancels_requested > 0) {
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);
}
diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c
index a626bda..2c9ef4b 100644
--- a/Modules/_datetimemodule.c
+++ b/Modules/_datetimemodule.c
@@ -416,6 +416,10 @@ iso_week1_monday(int year)
static int
iso_to_ymd(const int iso_year, const int iso_week, const int iso_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) {
int out_of_range = 1;
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
* -3: Failed to parse ISO week.
* -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;
p = parse_digits(p, year, 4);
@@ -3142,15 +3146,13 @@ date_fromisocalendar(PyObject *cls, PyObject *args, PyObject *kw)
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 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) {
PyErr_Format(PyExc_ValueError, "Invalid week: %d", week);