flpit 0.2.2__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flpit-0.2.2 → flpit-0.2.4}/PKG-INFO +1 -1
- {flpit-0.2.2 → flpit-0.2.4}/pyproject.toml +2 -2
- {flpit-0.2.2 → flpit-0.2.4}/src/flpit/core/linq.py +46 -39
- {flpit-0.2.2 → flpit-0.2.4}/src/flpit/task.py +64 -60
- {flpit-0.2.2 → flpit-0.2.4}/.gitignore +0 -0
- {flpit-0.2.2 → flpit-0.2.4}/LICENSE +0 -0
- {flpit-0.2.2 → flpit-0.2.4}/README.md +0 -0
- {flpit-0.2.2 → flpit-0.2.4}/src/flp/__init__.py +0 -0
- {flpit-0.2.2 → flpit-0.2.4}/src/flpit/__init__.py +0 -0
- {flpit-0.2.2 → flpit-0.2.4}/src/flpit/core/__init__.py +0 -0
- {flpit-0.2.2 → flpit-0.2.4}/src/flpit/csv.py +0 -0
- {flpit-0.2.2 → flpit-0.2.4}/src/flpit/flp.py +0 -0
- {flpit-0.2.2 → flpit-0.2.4}/src/flpit/json.py +0 -0
- {flpit-0.2.2 → flpit-0.2.4}/src/flpit/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flpit"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
description = "Fluent LINQ for Python — flip your iterables"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -55,7 +55,7 @@ include = ["src/flp", "src/flpit"]
|
|
|
55
55
|
[tool.pyrefly]
|
|
56
56
|
project-includes = ["src"]
|
|
57
57
|
project-excludes = ["**/__pycache__", "tests"]
|
|
58
|
-
search-path = ["src"]
|
|
58
|
+
search-path = ["src", "typing"]
|
|
59
59
|
preset = "strict"
|
|
60
60
|
|
|
61
61
|
[tool.pytest.ini_options]
|
|
@@ -14,7 +14,6 @@ from itertools import islice
|
|
|
14
14
|
from typing import (
|
|
15
15
|
Any,
|
|
16
16
|
Callable,
|
|
17
|
-
Generic,
|
|
18
17
|
Iterable,
|
|
19
18
|
Iterator,
|
|
20
19
|
Collection,
|
|
@@ -27,14 +26,14 @@ from typing import (
|
|
|
27
26
|
Union,
|
|
28
27
|
override,
|
|
29
28
|
overload,
|
|
30
|
-
cast as typing_cast,
|
|
29
|
+
cast as typing_cast,
|
|
30
|
+
BinaryIO,
|
|
31
|
+
Concatenate,
|
|
32
|
+
Sized
|
|
31
33
|
)
|
|
32
34
|
|
|
33
|
-
import flpit.json
|
|
34
35
|
from flpit.json import JsonConvert, JsonSerializerOptions
|
|
35
36
|
|
|
36
|
-
TItem = TypeVar("TItem")
|
|
37
|
-
TResult = TypeVar("TResult")
|
|
38
37
|
TKey = TypeVar("TKey")
|
|
39
38
|
TOther = TypeVar("TOther")
|
|
40
39
|
TAccumulate = TypeVar("TAccumulate")
|
|
@@ -114,16 +113,24 @@ class ArgumentOutOfBoundsError(ArgumentError):
|
|
|
114
113
|
super().__init__("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection.")
|
|
115
114
|
|
|
116
115
|
|
|
117
|
-
def _guard_empty
|
|
118
|
-
|
|
116
|
+
def _guard_empty[SelfT: Sized, R, **P](
|
|
117
|
+
func: Callable[Concatenate[SelfT, P], R],
|
|
118
|
+
) -> Callable[Concatenate[SelfT, P], R]:
|
|
119
|
+
"""Translate empty-sequence ValueError into EmptySequenceError."""
|
|
120
|
+
|
|
119
121
|
@wraps(func)
|
|
120
|
-
def wrapper(
|
|
122
|
+
def wrapper(
|
|
123
|
+
self: SelfT,
|
|
124
|
+
*args: P.args,
|
|
125
|
+
**kwargs: P.kwargs,
|
|
126
|
+
) -> R:
|
|
121
127
|
try:
|
|
122
128
|
return func(self, *args, **kwargs)
|
|
123
129
|
except ValueError as e:
|
|
124
|
-
if
|
|
130
|
+
if len(self) == 0:
|
|
125
131
|
raise EmptySequenceError() from e
|
|
126
132
|
raise
|
|
133
|
+
|
|
127
134
|
return wrapper
|
|
128
135
|
|
|
129
136
|
|
|
@@ -137,7 +144,7 @@ def dotnet_context() -> AbstractContextManager[decimal.Context]:
|
|
|
137
144
|
return decimal.localcontext(ctx)
|
|
138
145
|
|
|
139
146
|
|
|
140
|
-
class _FactoryIterable
|
|
147
|
+
class _FactoryIterable[TItem](Iterable[TItem]):
|
|
141
148
|
"""
|
|
142
149
|
Wraps a generator factory function into a clean Iterable[T].
|
|
143
150
|
Ensures __iter__ produces a brand-new iterator instance on every call
|
|
@@ -152,7 +159,7 @@ class _FactoryIterable(Iterable[TItem], Generic[TItem]):
|
|
|
152
159
|
return self._factory()
|
|
153
160
|
|
|
154
161
|
|
|
155
|
-
class FlpIt
|
|
162
|
+
class FlpIt[TItem](Iterable[TItem]):
|
|
156
163
|
"""
|
|
157
164
|
| Fluent Iterable
|
|
158
165
|
Lazy evaluation wrapper around an iterable, inspired by .NET LINQ's
|
|
@@ -173,7 +180,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
173
180
|
return iter(self._iterable)
|
|
174
181
|
|
|
175
182
|
# noinspection unused-parameter
|
|
176
|
-
def as_type(self, target_type: Type[TResult]) -> FlpIt[TResult]:
|
|
183
|
+
def as_type[TResult](self, target_type: Type[TResult]) -> FlpIt[TResult]:
|
|
177
184
|
"""Zero-cost static type hint update returning self directly without checks or allocations."""
|
|
178
185
|
return self # type: ignore[return-value]
|
|
179
186
|
|
|
@@ -257,7 +264,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
257
264
|
# return typing_cast(FlpIt[TItem], FlpIt[TItem](_FactoryIterable(_generator)))
|
|
258
265
|
return FlpIt[TItem](_FactoryIterable(_generator))
|
|
259
266
|
|
|
260
|
-
def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
|
|
267
|
+
def select[TResult](self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
|
|
261
268
|
"""Projects each element into a new form."""
|
|
262
269
|
def _generator() -> Iterator[TResult]:
|
|
263
270
|
for item in self:
|
|
@@ -265,7 +272,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
265
272
|
|
|
266
273
|
return FlpIt(_FactoryIterable(_generator))
|
|
267
274
|
|
|
268
|
-
def select_many(
|
|
275
|
+
def select_many[TResult](
|
|
269
276
|
self, selector: Callable[[TItem], Iterable[TResult]]
|
|
270
277
|
) -> FlpIt[TResult]:
|
|
271
278
|
"""Flattens sequence projections."""
|
|
@@ -310,7 +317,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
310
317
|
|
|
311
318
|
return FlpIt(_FactoryIterable(_generator))
|
|
312
319
|
|
|
313
|
-
def cast(self, target_type: Type[TResult]) -> FlpIt[TResult]:
|
|
320
|
+
def cast[TResult](self, target_type: Type[TResult]) -> FlpIt[TResult]:
|
|
314
321
|
"""Casts elements to a specified type or raises TypeError if cast fails."""
|
|
315
322
|
def _generator() -> Iterator[TResult]:
|
|
316
323
|
for item in self:
|
|
@@ -322,7 +329,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
322
329
|
|
|
323
330
|
return FlpIt(_FactoryIterable(_generator))
|
|
324
331
|
|
|
325
|
-
def of_type(self, target_type: Type[TResult]) -> FlpIt[TResult]:
|
|
332
|
+
def of_type[TResult](self, target_type: Type[TResult]) -> FlpIt[TResult]:
|
|
326
333
|
"""Filters the elements of an Iterable based on a specified type."""
|
|
327
334
|
def _generator() -> Iterator[TResult]:
|
|
328
335
|
for item in self:
|
|
@@ -360,7 +367,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
360
367
|
def zip(self, second: Iterable[TOther]) -> FlpIt[tuple[TItem, TOther]]: ...
|
|
361
368
|
|
|
362
369
|
@overload
|
|
363
|
-
def zip(
|
|
370
|
+
def zip[TResult, TOther](
|
|
364
371
|
self,
|
|
365
372
|
second: Iterable[TOther],
|
|
366
373
|
result_selector: Callable[[TItem, TOther], TResult],
|
|
@@ -598,7 +605,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
598
605
|
return item
|
|
599
606
|
raise EmptySequenceError() if predicate is _SENTINEL else NoMatchError()
|
|
600
607
|
|
|
601
|
-
def first_or_default(
|
|
608
|
+
def first_or_default[TResult](
|
|
602
609
|
self, default: TResult, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL
|
|
603
610
|
) -> Union[TItem, TResult]:
|
|
604
611
|
"""Returns the first element matching a predicate, or a default value."""
|
|
@@ -673,14 +680,7 @@ class _NoneOrderKey:
|
|
|
673
680
|
|
|
674
681
|
_NONE_ORDER_KEY = _NoneOrderKey()
|
|
675
682
|
|
|
676
|
-
|
|
677
|
-
selector: Callable[[TItem], Any],
|
|
678
|
-
item: TItem,
|
|
679
|
-
) -> Any:
|
|
680
|
-
key = selector(item)
|
|
681
|
-
return _NONE_ORDER_KEY if key is None else key
|
|
682
|
-
|
|
683
|
-
class OrderedIt(FlpIt[TItem]):
|
|
683
|
+
class OrderedIt[TItem](FlpIt[TItem]):
|
|
684
684
|
__slots__ = ("_criteria",)
|
|
685
685
|
|
|
686
686
|
def __init__(
|
|
@@ -701,13 +701,21 @@ class OrderedIt(FlpIt[TItem]):
|
|
|
701
701
|
else:
|
|
702
702
|
self._criteria = _criteria + ((selector, descending),)
|
|
703
703
|
|
|
704
|
+
@staticmethod
|
|
705
|
+
def __none_aware_key(
|
|
706
|
+
selector: Callable[[TItem], Any],
|
|
707
|
+
item: TItem,
|
|
708
|
+
) -> Any:
|
|
709
|
+
key = selector(item)
|
|
710
|
+
return _NONE_ORDER_KEY if key is None else key
|
|
711
|
+
|
|
704
712
|
@override
|
|
705
713
|
def __iter__(self) -> Iterator[TItem]:
|
|
706
714
|
items = list(self._iterable)
|
|
707
715
|
|
|
708
716
|
for selector, descending in reversed(self._criteria):
|
|
709
717
|
items.sort(
|
|
710
|
-
key=partial(
|
|
718
|
+
key=partial(self.__none_aware_key, selector),
|
|
711
719
|
reverse=descending,
|
|
712
720
|
)
|
|
713
721
|
|
|
@@ -736,7 +744,7 @@ class OrderedIt(FlpIt[TItem]):
|
|
|
736
744
|
)
|
|
737
745
|
|
|
738
746
|
|
|
739
|
-
class Grouping
|
|
747
|
+
class Grouping[TKey, TItem](FlpIt[TItem]):
|
|
740
748
|
"""Represents a collection of elements sharing a common key (.NET IGrouping<TKey, TElement>)."""
|
|
741
749
|
__slots__ = ("_key", )
|
|
742
750
|
|
|
@@ -760,7 +768,7 @@ class Grouping(FlpIt[TItem], Generic[TKey, TItem]):
|
|
|
760
768
|
return self.key == other.key and self.to_list() == other.to_list()
|
|
761
769
|
|
|
762
770
|
|
|
763
|
-
class FlpList
|
|
771
|
+
class FlpList[TItem](Collection[TItem]):
|
|
764
772
|
"""
|
|
765
773
|
| Fluent List
|
|
766
774
|
A materialized list extending UserList that yields lazy FlpIt instances for query operations.
|
|
@@ -781,7 +789,7 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
781
789
|
# =====================================================================
|
|
782
790
|
# 1. THE ABC CONTRACT
|
|
783
791
|
# =====================================================================
|
|
784
|
-
def __iter__(self) -> Iterator[
|
|
792
|
+
def __iter__(self) -> Iterator[TItem]:
|
|
785
793
|
return self.__list.__iter__()
|
|
786
794
|
|
|
787
795
|
def __len__(self) -> int:
|
|
@@ -1032,17 +1040,16 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
1032
1040
|
def concat(self, second: Iterable[TItem]) -> FlpIt[TItem]:
|
|
1033
1041
|
return FlpIt(self.__list.data).concat(second)
|
|
1034
1042
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
return self # type: ignore[return-value]
|
|
1043
|
+
def as_type[TResult](self, target_type: Type[TResult]) -> FlpList[TResult]:
|
|
1044
|
+
return typing_cast(FlpList[TResult], self)
|
|
1038
1045
|
|
|
1039
1046
|
def where(self, predicate: Callable[[TItem], bool]) -> FlpIt[TItem]:
|
|
1040
1047
|
return FlpIt[TItem](self.__list.data).where(predicate)
|
|
1041
1048
|
|
|
1042
|
-
def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
|
|
1049
|
+
def select[TResult](self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
|
|
1043
1050
|
return FlpIt(self.__list.data).select(selector)
|
|
1044
1051
|
|
|
1045
|
-
def select_many(
|
|
1052
|
+
def select_many[TResult](
|
|
1046
1053
|
self, selector: Callable[[TItem], Iterable[TResult]]
|
|
1047
1054
|
) -> FlpIt[TResult]:
|
|
1048
1055
|
return FlpIt(self.__list.data).select_many(selector)
|
|
@@ -1053,10 +1060,10 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
1053
1060
|
def take(self, count: int) -> FlpIt[TItem]:
|
|
1054
1061
|
return FlpIt(self.__list.data).take(count)
|
|
1055
1062
|
|
|
1056
|
-
def cast(self, target_type: Type[TResult]) -> FlpIt[TResult]:
|
|
1063
|
+
def cast[TResult](self, target_type: Type[TResult]) -> FlpIt[TResult]:
|
|
1057
1064
|
return FlpIt(self.__list.data).cast(target_type)
|
|
1058
1065
|
|
|
1059
|
-
def of_type(self, target_type: Type[TResult]) -> FlpIt[TResult]:
|
|
1066
|
+
def of_type[TResult](self, target_type: Type[TResult]) -> FlpIt[TResult]:
|
|
1060
1067
|
return FlpIt(self.__list.data).of_type(target_type)
|
|
1061
1068
|
|
|
1062
1069
|
def distinct(self) -> FlpIt[TItem]:
|
|
@@ -1071,7 +1078,7 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
1071
1078
|
def zip(self, second: Iterable[TOther]) -> FlpIt[tuple[TItem, TOther]]: ...
|
|
1072
1079
|
|
|
1073
1080
|
@overload
|
|
1074
|
-
def zip(
|
|
1081
|
+
def zip[TResult, TOther](
|
|
1075
1082
|
self,
|
|
1076
1083
|
second: Iterable[TOther],
|
|
1077
1084
|
result_selector: Callable[[TItem, TOther], TResult],
|
|
@@ -1204,7 +1211,7 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
1204
1211
|
|
|
1205
1212
|
raise EmptySequenceError() if predicate is _SENTINEL else NoMatchError()
|
|
1206
1213
|
|
|
1207
|
-
def first_or_default(
|
|
1214
|
+
def first_or_default[TResult](
|
|
1208
1215
|
self, default: TResult, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL
|
|
1209
1216
|
) -> TItem | TResult:
|
|
1210
1217
|
return FlpIt(self.__list.data).first_or_default(default, predicate)
|
|
@@ -49,7 +49,7 @@ from collections.abc import Awaitable, Callable, Coroutine
|
|
|
49
49
|
import flpit
|
|
50
50
|
|
|
51
51
|
T = TypeVar("T")
|
|
52
|
-
_LOG = logging.getLogger("
|
|
52
|
+
_LOG = logging.getLogger("task")
|
|
53
53
|
_MISSING = object()
|
|
54
54
|
_WAKE = object()
|
|
55
55
|
|
|
@@ -71,7 +71,7 @@ class OperationCanceledError(Exception):
|
|
|
71
71
|
super().__init__(message)
|
|
72
72
|
|
|
73
73
|
|
|
74
|
-
# Backwards-compatible spelling used by the first
|
|
74
|
+
# Backwards-compatible spelling used by the first Task prototype.
|
|
75
75
|
CancelledError = OperationCanceledError
|
|
76
76
|
|
|
77
77
|
|
|
@@ -236,7 +236,7 @@ class CancellationToken:
|
|
|
236
236
|
|
|
237
237
|
def throw_if_cancellation_requested(self) -> None:
|
|
238
238
|
if self.is_cancellation_requested:
|
|
239
|
-
raise OperationCanceledError("Cancellation was requested")
|
|
239
|
+
raise OperationCanceledError("Cancellation was requested", token=self)
|
|
240
240
|
|
|
241
241
|
def register(self, callback: Callable[[], Any]) -> CancellationTokenRegistration:
|
|
242
242
|
if not callable(callback):
|
|
@@ -393,8 +393,13 @@ class _Operation:
|
|
|
393
393
|
|
|
394
394
|
|
|
395
395
|
class _Sleep(_Operation):
|
|
396
|
-
def __init__(
|
|
397
|
-
|
|
396
|
+
def __init__(
|
|
397
|
+
self,
|
|
398
|
+
runtime: "_Runtime",
|
|
399
|
+
delay: float,
|
|
400
|
+
token: CancellationToken | None = None,
|
|
401
|
+
) -> None:
|
|
402
|
+
super().__init__(runtime, token)
|
|
398
403
|
self.delay = delay
|
|
399
404
|
self.deadline = time.monotonic() + delay
|
|
400
405
|
|
|
@@ -471,8 +476,12 @@ async def _invoke_background(
|
|
|
471
476
|
return result
|
|
472
477
|
|
|
473
478
|
|
|
474
|
-
async def _delay_coroutine(
|
|
475
|
-
|
|
479
|
+
async def _delay_coroutine(
|
|
480
|
+
runtime: "_Runtime",
|
|
481
|
+
delay: float,
|
|
482
|
+
token: CancellationToken | None = None,
|
|
483
|
+
) -> None:
|
|
484
|
+
await _Sleep(runtime, delay, token=token)
|
|
476
485
|
|
|
477
486
|
|
|
478
487
|
class Task(Generic[T]):
|
|
@@ -487,11 +496,10 @@ class Task(Generic[T]):
|
|
|
487
496
|
self,
|
|
488
497
|
runtime: "_Runtime",
|
|
489
498
|
*,
|
|
490
|
-
name: str | None = None,
|
|
491
499
|
token: CancellationToken | None = None,
|
|
492
500
|
) -> None:
|
|
493
501
|
self._runtime = runtime
|
|
494
|
-
self.name =
|
|
502
|
+
self.name = f"Task-{id(self):x}"
|
|
495
503
|
self._state = self.PENDING
|
|
496
504
|
self._result: Any = None
|
|
497
505
|
self._exception: BaseException | None = None
|
|
@@ -560,17 +568,42 @@ class Task(Generic[T]):
|
|
|
560
568
|
raise TypeError("handler must be callable or None")
|
|
561
569
|
_get_runtime().set_exception_handler(handler)
|
|
562
570
|
|
|
571
|
+
@overload
|
|
572
|
+
@classmethod
|
|
573
|
+
def run[R](
|
|
574
|
+
cls,
|
|
575
|
+
work: Callable[..., Coroutine[Any, Any, R]],
|
|
576
|
+
/,
|
|
577
|
+
*args: Any,
|
|
578
|
+
token: CancellationToken | None = None,
|
|
579
|
+
pass_token: bool = False,
|
|
580
|
+
**kwargs: Any,
|
|
581
|
+
) -> Task[R]: ...
|
|
582
|
+
|
|
583
|
+
@overload
|
|
584
|
+
@classmethod
|
|
585
|
+
def run[R](
|
|
586
|
+
cls,
|
|
587
|
+
work: Callable[..., R],
|
|
588
|
+
/,
|
|
589
|
+
*args: Any,
|
|
590
|
+
token: CancellationToken | None = None,
|
|
591
|
+
pass_token: bool = False,
|
|
592
|
+
**kwargs: Any,
|
|
593
|
+
) -> Task[R]: ...
|
|
594
|
+
|
|
563
595
|
@classmethod
|
|
564
596
|
def run(
|
|
565
597
|
cls,
|
|
566
|
-
work: Callable[...,
|
|
598
|
+
work: Callable[..., Any],
|
|
567
599
|
/,
|
|
568
600
|
*args: Any,
|
|
569
|
-
name: str | None = None,
|
|
570
601
|
token: CancellationToken | None = None,
|
|
571
602
|
pass_token: bool = False,
|
|
572
603
|
**kwargs: Any,
|
|
573
|
-
) ->
|
|
604
|
+
) -> Task[Any]:
|
|
605
|
+
# Keep your existing implementation here.
|
|
606
|
+
...
|
|
574
607
|
"""Schedule a coroutine/awaitable or ordinary callable.
|
|
575
608
|
|
|
576
609
|
Ordinary functions run in the bounded worker pool. ``token`` controls
|
|
@@ -594,7 +627,7 @@ class Task(Generic[T]):
|
|
|
594
627
|
work.close()
|
|
595
628
|
raise ValueError("pass_token=True requires an explicit token")
|
|
596
629
|
runtime = _get_runtime()
|
|
597
|
-
task = cls(runtime,
|
|
630
|
+
task = cls(runtime, token=token)
|
|
598
631
|
task._pass_token = pass_token
|
|
599
632
|
runtime._submit(task, work, args, kwargs)
|
|
600
633
|
return task
|
|
@@ -602,9 +635,7 @@ class Task(Generic[T]):
|
|
|
602
635
|
@classmethod
|
|
603
636
|
def when_all(
|
|
604
637
|
cls,
|
|
605
|
-
*tasks:
|
|
606
|
-
name: str | None = None,
|
|
607
|
-
token: CancellationToken | None = None,
|
|
638
|
+
*tasks: Awaitable[T]
|
|
608
639
|
) -> "Task[list[Any]]":
|
|
609
640
|
"""Complete when every supplied task completes, preserving input order.
|
|
610
641
|
|
|
@@ -615,22 +646,16 @@ class Task(Generic[T]):
|
|
|
615
646
|
several children fail, TaskAggregateError exposes all their exceptions.
|
|
616
647
|
If there are no tasks, the result is an empty list.
|
|
617
648
|
"""
|
|
618
|
-
if token is not None and not isinstance(token, CancellationToken):
|
|
619
|
-
raise TypeError("token must be a CancellationToken")
|
|
620
649
|
normalized = _normalize_tasks(tasks, allow_empty=True)
|
|
621
650
|
return cast(
|
|
622
651
|
"Task[list[Any]]",
|
|
623
|
-
|
|
624
|
-
# pyrefly: ignore [bad-argument-type]
|
|
625
|
-
cls.run(_when_all_coroutine, normalized, name=name, token=token),
|
|
652
|
+
cls.run(_when_all_coroutine, normalized),
|
|
626
653
|
)
|
|
627
654
|
|
|
628
655
|
@classmethod
|
|
629
656
|
def when_any(
|
|
630
657
|
cls,
|
|
631
658
|
*tasks: Any,
|
|
632
|
-
name: str | None = None,
|
|
633
|
-
token: CancellationToken | None = None,
|
|
634
659
|
) -> "Task[Task[Any]]":
|
|
635
660
|
"""Complete when one supplied task completes and return that task.
|
|
636
661
|
|
|
@@ -642,14 +667,10 @@ class Task(Generic[T]):
|
|
|
642
667
|
If multiple tasks are already complete at observation time, input order
|
|
643
668
|
breaks the tie deterministically.
|
|
644
669
|
"""
|
|
645
|
-
if token is not None and not isinstance(token, CancellationToken):
|
|
646
|
-
raise TypeError("token must be a CancellationToken")
|
|
647
670
|
normalized = _normalize_tasks(tasks, allow_empty=False)
|
|
648
671
|
return cast(
|
|
649
672
|
"Task[Task[Any]]",
|
|
650
|
-
|
|
651
|
-
# pyrefly: ignore [bad-argument-type]
|
|
652
|
-
cls.run(_when_any_coroutine, normalized, name=name, token=token),
|
|
673
|
+
cls.run(_when_any_coroutine, normalized),
|
|
653
674
|
)
|
|
654
675
|
|
|
655
676
|
@staticmethod
|
|
@@ -674,7 +695,6 @@ class Task(Generic[T]):
|
|
|
674
695
|
seconds: float,
|
|
675
696
|
*,
|
|
676
697
|
token: CancellationToken | None = None,
|
|
677
|
-
name: str | None = None,
|
|
678
698
|
) -> "Task[None]":
|
|
679
699
|
"""Return a task completed after seconds without blocking a worker."""
|
|
680
700
|
if seconds < 0:
|
|
@@ -682,12 +702,12 @@ class Task(Generic[T]):
|
|
|
682
702
|
if token is not None and not isinstance(token, CancellationToken):
|
|
683
703
|
raise TypeError("token must be a CancellationToken")
|
|
684
704
|
runtime = _get_runtime()
|
|
685
|
-
task: Task[None] = cls(runtime,
|
|
705
|
+
task: Task[None] = cls(runtime, token=token)
|
|
686
706
|
# During graceful cancellation, allow an already-running coroutine to
|
|
687
707
|
# create a delay for async cleanup. External callers are still rejected.
|
|
688
708
|
runtime._submit_coroutine(
|
|
689
709
|
task,
|
|
690
|
-
_delay_coroutine(runtime, seconds),
|
|
710
|
+
_delay_coroutine(runtime, seconds, token),
|
|
691
711
|
allow_during_shutdown=runtime.is_runtime_thread(),
|
|
692
712
|
)
|
|
693
713
|
return task
|
|
@@ -754,16 +774,6 @@ class Task(Generic[T]):
|
|
|
754
774
|
return self._exception
|
|
755
775
|
return OperationCanceledError(f"{self.name} was cancelled")
|
|
756
776
|
|
|
757
|
-
def cancel(self) -> bool:
|
|
758
|
-
"""Request cancellation of this Task, independently of other token users."""
|
|
759
|
-
if self.done():
|
|
760
|
-
return False
|
|
761
|
-
try:
|
|
762
|
-
self._runtime._post(lambda: self._cancel_on_runtime(), internal=True)
|
|
763
|
-
except RuntimeShutdownError:
|
|
764
|
-
return False
|
|
765
|
-
return True
|
|
766
|
-
|
|
767
777
|
def __await__(self) -> Generator[Any, T, T]:
|
|
768
778
|
if self.done():
|
|
769
779
|
return self._raise_or_return()
|
|
@@ -780,11 +790,6 @@ class Task(Generic[T]):
|
|
|
780
790
|
raise InvalidStateError(f"{self.name} has not completed")
|
|
781
791
|
return cast(T, self._result)
|
|
782
792
|
|
|
783
|
-
def _bind_token(self) -> None:
|
|
784
|
-
if not self._token.can_be_cancelled or self._token_registration is not None:
|
|
785
|
-
return
|
|
786
|
-
self._token_registration = self._token.register(self.cancel)
|
|
787
|
-
|
|
788
793
|
def _cancel_on_runtime(self) -> None:
|
|
789
794
|
if self.done():
|
|
790
795
|
return
|
|
@@ -1152,7 +1157,7 @@ class _Runtime:
|
|
|
1152
1157
|
self._max_pending_background = max_pending_background
|
|
1153
1158
|
self._pending_background: deque[tuple[Task[Any], _BackgroundCall]] = deque()
|
|
1154
1159
|
self._background_active = 0 # scheduler-thread-owned submitted call count
|
|
1155
|
-
self._executor = ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix="
|
|
1160
|
+
self._executor = ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix="Thread")
|
|
1156
1161
|
self._inflight_lock = threading.Condition()
|
|
1157
1162
|
self._inflight_futures: set[Future[Any]] = set()
|
|
1158
1163
|
self._current_task: Task[Any] | None = None
|
|
@@ -1181,8 +1186,8 @@ class _Runtime:
|
|
|
1181
1186
|
return
|
|
1182
1187
|
with self._state_lock:
|
|
1183
1188
|
if self._closed or self._shutdown_requested:
|
|
1184
|
-
raise RuntimeShutdownError("
|
|
1185
|
-
self._thread = threading.Thread(target=self._loop, name="
|
|
1189
|
+
raise RuntimeShutdownError("Task runtime is shutting down")
|
|
1190
|
+
self._thread = threading.Thread(target=self._loop, name="Task-runtime", daemon=True)
|
|
1186
1191
|
self._thread.start()
|
|
1187
1192
|
self._started.wait()
|
|
1188
1193
|
|
|
@@ -1199,8 +1204,8 @@ class _Runtime:
|
|
|
1199
1204
|
handler({**context, "exception": error})
|
|
1200
1205
|
return
|
|
1201
1206
|
except Exception:
|
|
1202
|
-
_LOG.exception("
|
|
1203
|
-
message = str(context.get("message", "Unexpected
|
|
1207
|
+
_LOG.exception("Task exception handler itself failed")
|
|
1208
|
+
message = str(context.get("message", "Unexpected Task runtime error"))
|
|
1204
1209
|
_LOG.error("%s", message, exc_info=(type(error), error, error.__traceback__))
|
|
1205
1210
|
|
|
1206
1211
|
def _call_sync(self, callback: Callable[[], T], *, internal: bool = False) -> T:
|
|
@@ -1230,7 +1235,7 @@ class _Runtime:
|
|
|
1230
1235
|
thread = self._thread
|
|
1231
1236
|
if thread is None or not thread.is_alive():
|
|
1232
1237
|
raise RuntimeShutdownError(
|
|
1233
|
-
"
|
|
1238
|
+
"Task runtime stopped before the scheduled operation ran"
|
|
1234
1239
|
)
|
|
1235
1240
|
if "error" in outcome:
|
|
1236
1241
|
raise outcome["error"]
|
|
@@ -1241,7 +1246,7 @@ class _Runtime:
|
|
|
1241
1246
|
execute_inline = False
|
|
1242
1247
|
with self._state_lock:
|
|
1243
1248
|
if self._closed or self._stopping or (self._shutdown_requested and not internal):
|
|
1244
|
-
raise RuntimeShutdownError("
|
|
1249
|
+
raise RuntimeShutdownError("Task runtime is shutting down")
|
|
1245
1250
|
if self.is_runtime_thread():
|
|
1246
1251
|
execute_inline = True
|
|
1247
1252
|
else:
|
|
@@ -1312,7 +1317,6 @@ class _Runtime:
|
|
|
1312
1317
|
return
|
|
1313
1318
|
task._coroutine = coroutine
|
|
1314
1319
|
self._tasks.add(task)
|
|
1315
|
-
task._bind_token()
|
|
1316
1320
|
self._schedule(task)
|
|
1317
1321
|
|
|
1318
1322
|
def _schedule(
|
|
@@ -1361,7 +1365,7 @@ class _Runtime:
|
|
|
1361
1365
|
if not isinstance(yielded, _Operation):
|
|
1362
1366
|
raise RuntimeError(
|
|
1363
1367
|
f"{task.name} yielded unsupported object {yielded!r}; "
|
|
1364
|
-
"this runtime only supports
|
|
1368
|
+
"this runtime only supports Task awaitables"
|
|
1365
1369
|
)
|
|
1366
1370
|
if yielded.runtime is not self:
|
|
1367
1371
|
raise RuntimeError("awaitable belongs to a different runtime")
|
|
@@ -1614,7 +1618,7 @@ class _Runtime:
|
|
|
1614
1618
|
callback()
|
|
1615
1619
|
except BaseException as error:
|
|
1616
1620
|
self._report_exception(
|
|
1617
|
-
{"message": "Unhandled exception in
|
|
1621
|
+
{"message": "Unhandled exception in Task scheduler callback", "callback": callback},
|
|
1618
1622
|
error,
|
|
1619
1623
|
)
|
|
1620
1624
|
|
|
@@ -1704,7 +1708,7 @@ class _Runtime:
|
|
|
1704
1708
|
if self._stopping or self.closed:
|
|
1705
1709
|
break
|
|
1706
1710
|
self._report_exception(
|
|
1707
|
-
{"message": "Selector failed; stopping
|
|
1711
|
+
{"message": "Selector failed; stopping Task runtime"}, error
|
|
1708
1712
|
)
|
|
1709
1713
|
self._stopping = True
|
|
1710
1714
|
break
|
|
@@ -1716,7 +1720,7 @@ class _Runtime:
|
|
|
1716
1720
|
)
|
|
1717
1721
|
self._stopping = True
|
|
1718
1722
|
except BaseException as error:
|
|
1719
|
-
self._report_exception({"message": "Fatal error in
|
|
1723
|
+
self._report_exception({"message": "Fatal error in Task scheduler loop"}, error)
|
|
1720
1724
|
self._stopping = True
|
|
1721
1725
|
finally:
|
|
1722
1726
|
# Stop accepting work before releasing scheduler resources. The
|
|
@@ -1771,7 +1775,7 @@ class _Runtime:
|
|
|
1771
1775
|
runtime_stopped = thread is None or not thread.is_alive()
|
|
1772
1776
|
if not runtime_stopped:
|
|
1773
1777
|
self._report_exception(
|
|
1774
|
-
{"message": "
|
|
1778
|
+
{"message": "Task scheduler thread did not stop before timeout"},
|
|
1775
1779
|
TimeoutError("scheduler shutdown timed out"),
|
|
1776
1780
|
)
|
|
1777
1781
|
|
|
@@ -1825,7 +1829,7 @@ def _shutdown_at_exit() -> None:
|
|
|
1825
1829
|
try:
|
|
1826
1830
|
Task.shutdown(grace_timeout=0.2, worker_timeout=0.2)
|
|
1827
1831
|
except BaseException:
|
|
1828
|
-
_LOG.exception("Error during
|
|
1832
|
+
_LOG.exception("Error during Task interpreter shutdown")
|
|
1829
1833
|
|
|
1830
1834
|
|
|
1831
1835
|
atexit.register(_shutdown_at_exit)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|