mirror of https://github.com/python/cpython
Merge branch 'main' of https://github.com/python/cpython
This commit is contained in:
commit
d0f93d132f
|
@ -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.,
|
||||
|
|
|
@ -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``.
|
||||
``TypeIs`` and ``TypeGuard`` differ in the following ways:
|
||||
|
||||
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`` 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
|
||||
|
||||
|
|
|
@ -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`.)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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):
|
||||
|
|
80
Lib/enum.py
80
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:
|
||||
# an alias to an existing member
|
||||
enum_class(value)._add_alias_(name)
|
||||
# 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:
|
||||
# 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
|
||||
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
|
||||
contained._add_alias_(name)
|
||||
else:
|
||||
# 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
|
||||
try:
|
||||
contained = value in value2member_map
|
||||
except TypeError:
|
||||
contained = value in unhashable_values
|
||||
if contained:
|
||||
# an alias to an existing member
|
||||
enum_class(value)._add_alias_(name)
|
||||
# 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:
|
||||
# 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
|
||||
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
|
||||
contained._add_alias_(name)
|
||||
else:
|
||||
# finish creating member
|
||||
member._name_ = name
|
||||
member.__objclass__ = enum_class
|
||||
member.__init__(value)
|
||||
|
|
|
@ -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
|
||||
]
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -496,6 +496,7 @@ David Edelsohn
|
|||
John Edmonds
|
||||
Benjamin Edwards
|
||||
Grant Edwards
|
||||
Vlad Efanov
|
||||
Zvi Effron
|
||||
John Ehresman
|
||||
Tal Einat
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Improve validation logic in the C implementation of :meth:`datetime.fromisoformat`
|
||||
to better handle invalid years. Patch by Vlad Efanov.
|
|
@ -0,0 +1 @@
|
|||
Add :data:`typing.TypeIs`, implementing :pep:`742`. Patch by Jelle Zijlstra.
|
|
@ -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.
|
|
@ -0,0 +1,2 @@
|
|||
Fix ``_simple_enum`` to detect aliases when multiple arguments are present
|
||||
but only one is the member value.
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue