prefect-client 3.1.5__py3-none-any.whl → 3.1.6__py3-none-any.whl

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.
Files changed (93) hide show
  1. prefect/__init__.py +3 -0
  2. prefect/_internal/compatibility/migration.py +1 -1
  3. prefect/_internal/concurrency/api.py +52 -52
  4. prefect/_internal/concurrency/calls.py +59 -35
  5. prefect/_internal/concurrency/cancellation.py +34 -18
  6. prefect/_internal/concurrency/event_loop.py +7 -6
  7. prefect/_internal/concurrency/threads.py +41 -33
  8. prefect/_internal/concurrency/waiters.py +28 -21
  9. prefect/_internal/pydantic/v1_schema.py +2 -2
  10. prefect/_internal/pydantic/v2_schema.py +10 -9
  11. prefect/_internal/schemas/bases.py +9 -7
  12. prefect/_internal/schemas/validators.py +2 -1
  13. prefect/_version.py +3 -3
  14. prefect/automations.py +53 -47
  15. prefect/blocks/abstract.py +12 -10
  16. prefect/blocks/core.py +4 -2
  17. prefect/cache_policies.py +11 -11
  18. prefect/client/__init__.py +3 -1
  19. prefect/client/base.py +36 -37
  20. prefect/client/cloud.py +26 -19
  21. prefect/client/collections.py +2 -2
  22. prefect/client/orchestration.py +342 -273
  23. prefect/client/schemas/__init__.py +24 -0
  24. prefect/client/schemas/actions.py +123 -116
  25. prefect/client/schemas/objects.py +110 -81
  26. prefect/client/schemas/responses.py +18 -18
  27. prefect/client/schemas/schedules.py +136 -93
  28. prefect/client/subscriptions.py +28 -14
  29. prefect/client/utilities.py +32 -36
  30. prefect/concurrency/asyncio.py +6 -9
  31. prefect/concurrency/sync.py +35 -5
  32. prefect/context.py +39 -31
  33. prefect/deployments/flow_runs.py +3 -5
  34. prefect/docker/__init__.py +1 -1
  35. prefect/events/schemas/events.py +25 -20
  36. prefect/events/utilities.py +1 -2
  37. prefect/filesystems.py +3 -3
  38. prefect/flow_engine.py +61 -21
  39. prefect/flow_runs.py +3 -3
  40. prefect/flows.py +214 -170
  41. prefect/logging/configuration.py +1 -1
  42. prefect/logging/highlighters.py +1 -2
  43. prefect/logging/loggers.py +30 -20
  44. prefect/main.py +17 -24
  45. prefect/runner/runner.py +43 -21
  46. prefect/runner/server.py +30 -32
  47. prefect/runner/submit.py +3 -6
  48. prefect/runner/utils.py +6 -6
  49. prefect/runtime/flow_run.py +7 -0
  50. prefect/settings/constants.py +2 -2
  51. prefect/settings/legacy.py +1 -1
  52. prefect/settings/models/server/events.py +10 -0
  53. prefect/task_engine.py +72 -19
  54. prefect/task_runners.py +2 -2
  55. prefect/tasks.py +46 -33
  56. prefect/telemetry/bootstrap.py +15 -2
  57. prefect/telemetry/run_telemetry.py +107 -0
  58. prefect/transactions.py +14 -14
  59. prefect/types/__init__.py +1 -4
  60. prefect/utilities/_engine.py +96 -0
  61. prefect/utilities/annotations.py +25 -18
  62. prefect/utilities/asyncutils.py +126 -140
  63. prefect/utilities/callables.py +87 -78
  64. prefect/utilities/collections.py +278 -117
  65. prefect/utilities/compat.py +13 -21
  66. prefect/utilities/context.py +6 -5
  67. prefect/utilities/dispatch.py +23 -12
  68. prefect/utilities/dockerutils.py +33 -32
  69. prefect/utilities/engine.py +126 -239
  70. prefect/utilities/filesystem.py +18 -15
  71. prefect/utilities/hashing.py +10 -11
  72. prefect/utilities/importtools.py +40 -27
  73. prefect/utilities/math.py +9 -5
  74. prefect/utilities/names.py +3 -3
  75. prefect/utilities/processutils.py +121 -57
  76. prefect/utilities/pydantic.py +41 -36
  77. prefect/utilities/render_swagger.py +22 -12
  78. prefect/utilities/schema_tools/__init__.py +2 -1
  79. prefect/utilities/schema_tools/hydration.py +50 -43
  80. prefect/utilities/schema_tools/validation.py +52 -42
  81. prefect/utilities/services.py +13 -12
  82. prefect/utilities/templating.py +45 -45
  83. prefect/utilities/text.py +2 -1
  84. prefect/utilities/timeout.py +4 -4
  85. prefect/utilities/urls.py +9 -4
  86. prefect/utilities/visualization.py +46 -24
  87. prefect/variables.py +9 -8
  88. prefect/workers/base.py +15 -8
  89. {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/METADATA +4 -2
  90. {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/RECORD +93 -91
  91. {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/LICENSE +0 -0
  92. {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/WHEEL +0 -0
  93. {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/top_level.txt +0 -0
@@ -6,23 +6,12 @@ import asyncio
6
6
  import inspect
7
7
  import threading
8
8
  import warnings
9
- from concurrent.futures import ThreadPoolExecutor
10
- from contextlib import asynccontextmanager
11
- from contextvars import ContextVar, copy_context
9
+ from collections.abc import AsyncGenerator, Awaitable, Coroutine
10
+ from contextlib import AbstractAsyncContextManager, asynccontextmanager
11
+ from contextvars import ContextVar
12
12
  from functools import partial, wraps
13
- from typing import (
14
- Any,
15
- Awaitable,
16
- Callable,
17
- Coroutine,
18
- Dict,
19
- List,
20
- Optional,
21
- TypeVar,
22
- Union,
23
- cast,
24
- overload,
25
- )
13
+ from logging import Logger
14
+ from typing import TYPE_CHECKING, Any, Callable, NoReturn, Optional, Union, overload
26
15
  from uuid import UUID, uuid4
27
16
 
28
17
  import anyio
@@ -30,9 +19,18 @@ import anyio.abc
30
19
  import anyio.from_thread
31
20
  import anyio.to_thread
32
21
  import sniffio
33
- from typing_extensions import Literal, ParamSpec, TypeGuard
22
+ from typing_extensions import (
23
+ Literal,
24
+ ParamSpec,
25
+ Self,
26
+ TypeAlias,
27
+ TypeGuard,
28
+ TypeVar,
29
+ TypeVarTuple,
30
+ Unpack,
31
+ )
34
32
 
35
- from prefect._internal.concurrency.api import _cast_to_call, from_sync
33
+ from prefect._internal.concurrency.api import cast_to_call, from_sync
36
34
  from prefect._internal.concurrency.threads import (
37
35
  get_run_sync_loop,
38
36
  in_run_sync_loop,
@@ -41,62 +39,65 @@ from prefect.logging import get_logger
41
39
 
42
40
  T = TypeVar("T")
43
41
  P = ParamSpec("P")
44
- R = TypeVar("R")
42
+ R = TypeVar("R", infer_variance=True)
45
43
  F = TypeVar("F", bound=Callable[..., Any])
46
44
  Async = Literal[True]
47
45
  Sync = Literal[False]
48
46
  A = TypeVar("A", Async, Sync, covariant=True)
47
+ PosArgsT = TypeVarTuple("PosArgsT")
48
+
49
+ _SyncOrAsyncCallable: TypeAlias = Callable[P, Union[R, Awaitable[R]]]
49
50
 
50
51
  # Global references to prevent garbage collection for `add_event_loop_shutdown_callback`
51
- EVENT_LOOP_GC_REFS = {}
52
+ EVENT_LOOP_GC_REFS: dict[int, AsyncGenerator[None, Any]] = {}
52
53
 
53
- PREFECT_THREAD_LIMITER: Optional[anyio.CapacityLimiter] = None
54
54
 
55
55
  RUNNING_IN_RUN_SYNC_LOOP_FLAG = ContextVar("running_in_run_sync_loop", default=False)
56
56
  RUNNING_ASYNC_FLAG = ContextVar("run_async", default=False)
57
- BACKGROUND_TASKS: set[asyncio.Task] = set()
58
- background_task_lock = threading.Lock()
57
+ BACKGROUND_TASKS: set[asyncio.Task[Any]] = set()
58
+ background_task_lock: threading.Lock = threading.Lock()
59
59
 
60
60
  # Thread-local storage to keep track of worker thread state
61
61
  _thread_local = threading.local()
62
62
 
63
- logger = get_logger()
63
+ logger: Logger = get_logger()
64
+
65
+
66
+ _prefect_thread_limiter: Optional[anyio.CapacityLimiter] = None
64
67
 
65
68
 
66
- def get_thread_limiter():
67
- global PREFECT_THREAD_LIMITER
69
+ def get_thread_limiter() -> anyio.CapacityLimiter:
70
+ global _prefect_thread_limiter
68
71
 
69
- if PREFECT_THREAD_LIMITER is None:
70
- PREFECT_THREAD_LIMITER = anyio.CapacityLimiter(250)
72
+ if _prefect_thread_limiter is None:
73
+ _prefect_thread_limiter = anyio.CapacityLimiter(250)
71
74
 
72
- return PREFECT_THREAD_LIMITER
75
+ return _prefect_thread_limiter
73
76
 
74
77
 
75
78
  def is_async_fn(
76
- func: Union[Callable[P, R], Callable[P, Awaitable[R]]],
79
+ func: _SyncOrAsyncCallable[P, R],
77
80
  ) -> TypeGuard[Callable[P, Awaitable[R]]]:
78
81
  """
79
82
  Returns `True` if a function returns a coroutine.
80
83
 
81
84
  See https://github.com/microsoft/pyright/issues/2142 for an example use
82
85
  """
83
- while hasattr(func, "__wrapped__"):
84
- func = func.__wrapped__
85
-
86
+ func = inspect.unwrap(func)
86
87
  return asyncio.iscoroutinefunction(func)
87
88
 
88
89
 
89
- def is_async_gen_fn(func):
90
+ def is_async_gen_fn(
91
+ func: Callable[P, Any],
92
+ ) -> TypeGuard[Callable[P, AsyncGenerator[Any, Any]]]:
90
93
  """
91
94
  Returns `True` if a function is an async generator.
92
95
  """
93
- while hasattr(func, "__wrapped__"):
94
- func = func.__wrapped__
95
-
96
+ func = inspect.unwrap(func)
96
97
  return inspect.isasyncgenfunction(func)
97
98
 
98
99
 
99
- def create_task(coroutine: Coroutine) -> asyncio.Task:
100
+ def create_task(coroutine: Coroutine[Any, Any, R]) -> asyncio.Task[R]:
100
101
  """
101
102
  Replacement for asyncio.create_task that will ensure that tasks aren't
102
103
  garbage collected before they complete. Allows for "fire and forget"
@@ -122,68 +123,32 @@ def create_task(coroutine: Coroutine) -> asyncio.Task:
122
123
  return task
123
124
 
124
125
 
125
- def _run_sync_in_new_thread(coroutine: Coroutine[Any, Any, T]) -> T:
126
- """
127
- Note: this is an OLD implementation of `run_coro_as_sync` which liberally created
128
- new threads and new loops. This works, but prevents sharing any objects
129
- across coroutines, in particular httpx clients, which are very expensive to
130
- instantiate.
131
-
132
- This is here for historical purposes and can be removed if/when it is no
133
- longer needed for reference.
134
-
135
- ---
136
-
137
- Runs a coroutine from a synchronous context. A thread will be spawned to run
138
- the event loop if necessary, which allows coroutines to run in environments
139
- like Jupyter notebooks where the event loop runs on the main thread.
140
-
141
- Args:
142
- coroutine: The coroutine to run.
143
-
144
- Returns:
145
- The return value of the coroutine.
146
-
147
- Example:
148
- Basic usage: ```python async def my_async_function(x: int) -> int:
149
- return x + 1
150
-
151
- run_sync(my_async_function(1)) ```
152
- """
126
+ @overload
127
+ def run_coro_as_sync(
128
+ coroutine: Coroutine[Any, Any, R],
129
+ *,
130
+ force_new_thread: bool = ...,
131
+ wait_for_result: Literal[True] = ...,
132
+ ) -> R:
133
+ ...
153
134
 
154
- # ensure context variables are properly copied to the async frame
155
- async def context_local_wrapper():
156
- """
157
- Wrapper that is submitted using copy_context().run to ensure
158
- the RUNNING_ASYNC_FLAG mutations are tightly scoped to this coroutine's frame.
159
- """
160
- token = RUNNING_ASYNC_FLAG.set(True)
161
- try:
162
- result = await coroutine
163
- finally:
164
- RUNNING_ASYNC_FLAG.reset(token)
165
- return result
166
135
 
167
- context = copy_context()
168
- try:
169
- loop = asyncio.get_running_loop()
170
- except RuntimeError:
171
- loop = None
172
-
173
- if loop and loop.is_running():
174
- with ThreadPoolExecutor() as executor:
175
- future = executor.submit(context.run, asyncio.run, context_local_wrapper())
176
- result = cast(T, future.result())
177
- else:
178
- result = context.run(asyncio.run, context_local_wrapper())
179
- return result
136
+ @overload
137
+ def run_coro_as_sync(
138
+ coroutine: Coroutine[Any, Any, R],
139
+ *,
140
+ force_new_thread: bool = ...,
141
+ wait_for_result: Literal[False] = False,
142
+ ) -> R:
143
+ ...
180
144
 
181
145
 
182
146
  def run_coro_as_sync(
183
- coroutine: Awaitable[R],
147
+ coroutine: Coroutine[Any, Any, R],
148
+ *,
184
149
  force_new_thread: bool = False,
185
150
  wait_for_result: bool = True,
186
- ) -> Union[R, None]:
151
+ ) -> Optional[R]:
187
152
  """
188
153
  Runs a coroutine from a synchronous context, as if it were a synchronous
189
154
  function.
@@ -210,7 +175,7 @@ def run_coro_as_sync(
210
175
  The result of the coroutine if wait_for_result is True, otherwise None.
211
176
  """
212
177
 
213
- async def coroutine_wrapper() -> Union[R, None]:
178
+ async def coroutine_wrapper() -> Optional[R]:
214
179
  """
215
180
  Set flags so that children (and grandchildren...) of this task know they are running in a new
216
181
  thread and do not try to run on the run_sync thread, which would cause a
@@ -231,12 +196,13 @@ def run_coro_as_sync(
231
196
  # that is running in the run_sync loop, we need to run this coroutine in a
232
197
  # new thread
233
198
  if in_run_sync_loop() or RUNNING_IN_RUN_SYNC_LOOP_FLAG.get() or force_new_thread:
234
- return from_sync.call_in_new_thread(coroutine_wrapper)
199
+ result = from_sync.call_in_new_thread(coroutine_wrapper)
200
+ return result
235
201
 
236
202
  # otherwise, we can run the coroutine in the run_sync loop
237
203
  # and wait for the result
238
204
  else:
239
- call = _cast_to_call(coroutine_wrapper)
205
+ call = cast_to_call(coroutine_wrapper)
240
206
  runner = get_run_sync_loop()
241
207
  runner.submit(call)
242
208
  try:
@@ -249,8 +215,8 @@ def run_coro_as_sync(
249
215
 
250
216
 
251
217
  async def run_sync_in_worker_thread(
252
- __fn: Callable[..., T], *args: Any, **kwargs: Any
253
- ) -> T:
218
+ __fn: Callable[P, R], *args: P.args, **kwargs: P.kwargs
219
+ ) -> R:
254
220
  """
255
221
  Runs a sync function in a new worker thread so that the main thread's event loop
256
222
  is not blocked.
@@ -274,14 +240,14 @@ async def run_sync_in_worker_thread(
274
240
  RUNNING_ASYNC_FLAG.reset(token)
275
241
 
276
242
 
277
- def call_with_mark(call):
243
+ def call_with_mark(call: Callable[..., R]) -> R:
278
244
  mark_as_worker_thread()
279
245
  return call()
280
246
 
281
247
 
282
248
  def run_async_from_worker_thread(
283
- __fn: Callable[..., Awaitable[T]], *args: Any, **kwargs: Any
284
- ) -> T:
249
+ __fn: Callable[P, Awaitable[R]], *args: P.args, **kwargs: P.kwargs
250
+ ) -> R:
285
251
  """
286
252
  Runs an async function in the main thread's event loop, blocking the worker
287
253
  thread until completion
@@ -290,11 +256,13 @@ def run_async_from_worker_thread(
290
256
  return anyio.from_thread.run(call)
291
257
 
292
258
 
293
- def run_async_in_new_loop(__fn: Callable[..., Awaitable[T]], *args: Any, **kwargs: Any):
259
+ def run_async_in_new_loop(
260
+ __fn: Callable[P, Awaitable[R]], *args: P.args, **kwargs: P.kwargs
261
+ ) -> R:
294
262
  return anyio.run(partial(__fn, *args, **kwargs))
295
263
 
296
264
 
297
- def mark_as_worker_thread():
265
+ def mark_as_worker_thread() -> None:
298
266
  _thread_local.is_worker_thread = True
299
267
 
300
268
 
@@ -312,23 +280,9 @@ def in_async_main_thread() -> bool:
312
280
  return not in_async_worker_thread()
313
281
 
314
282
 
315
- @overload
316
- def sync_compatible(
317
- async_fn: Callable[..., Coroutine[Any, Any, R]],
318
- ) -> Callable[..., R]:
319
- ...
320
-
321
-
322
- @overload
323
283
  def sync_compatible(
324
- async_fn: Callable[..., Coroutine[Any, Any, R]],
325
- ) -> Callable[..., Coroutine[Any, Any, R]]:
326
- ...
327
-
328
-
329
- def sync_compatible(
330
- async_fn: Callable[..., Coroutine[Any, Any, R]],
331
- ) -> Callable[..., Union[R, Coroutine[Any, Any, R]]]:
284
+ async_fn: Callable[P, Coroutine[Any, Any, R]],
285
+ ) -> Callable[P, Union[R, Coroutine[Any, Any, R]]]:
332
286
  """
333
287
  Converts an async function into a dual async and sync function.
334
288
 
@@ -393,7 +347,7 @@ def sync_compatible(
393
347
 
394
348
  if _sync is True:
395
349
  return run_coro_as_sync(ctx_call())
396
- elif _sync is False or RUNNING_ASYNC_FLAG.get() or is_async:
350
+ elif RUNNING_ASYNC_FLAG.get() or is_async:
397
351
  return ctx_call()
398
352
  else:
399
353
  return run_coro_as_sync(ctx_call())
@@ -409,8 +363,24 @@ def sync_compatible(
409
363
  return wrapper
410
364
 
411
365
 
366
+ @overload
367
+ def asyncnullcontext(
368
+ value: None = None, *args: Any, **kwargs: Any
369
+ ) -> AbstractAsyncContextManager[None, None]:
370
+ ...
371
+
372
+
373
+ @overload
374
+ def asyncnullcontext(
375
+ value: R, *args: Any, **kwargs: Any
376
+ ) -> AbstractAsyncContextManager[R, None]:
377
+ ...
378
+
379
+
412
380
  @asynccontextmanager
413
- async def asyncnullcontext(value=None, *args, **kwargs):
381
+ async def asyncnullcontext(
382
+ value: Optional[R] = None, *args: Any, **kwargs: Any
383
+ ) -> AsyncGenerator[Any, Optional[R]]:
414
384
  yield value
415
385
 
416
386
 
@@ -426,7 +396,7 @@ def sync(__async_fn: Callable[P, Awaitable[T]], *args: P.args, **kwargs: P.kwarg
426
396
  "`sync` called from an asynchronous context; "
427
397
  "you should `await` the async function directly instead."
428
398
  )
429
- with anyio.start_blocking_portal() as portal:
399
+ with anyio.from_thread.start_blocking_portal() as portal:
430
400
  return portal.call(partial(__async_fn, *args, **kwargs))
431
401
  elif in_async_worker_thread():
432
402
  # In a sync context but we can access the event loop thread; send the async
@@ -438,7 +408,9 @@ def sync(__async_fn: Callable[P, Awaitable[T]], *args: P.args, **kwargs: P.kwarg
438
408
  return run_async_in_new_loop(__async_fn, *args, **kwargs)
439
409
 
440
410
 
441
- async def add_event_loop_shutdown_callback(coroutine_fn: Callable[[], Awaitable]):
411
+ async def add_event_loop_shutdown_callback(
412
+ coroutine_fn: Callable[[], Awaitable[Any]],
413
+ ) -> None:
442
414
  """
443
415
  Adds a callback to the given callable on event loop closure. The callable must be
444
416
  a coroutine function. It will be awaited when the current event loop is shutting
@@ -454,7 +426,7 @@ async def add_event_loop_shutdown_callback(coroutine_fn: Callable[[], Awaitable]
454
426
  loop is about to close.
455
427
  """
456
428
 
457
- async def on_shutdown(key):
429
+ async def on_shutdown(key: int) -> AsyncGenerator[None, Any]:
458
430
  # It appears that EVENT_LOOP_GC_REFS is somehow being garbage collected early.
459
431
  # We hold a reference to it so as to preserve it, at least for the lifetime of
460
432
  # this coroutine. See the issue below for the initial report/discussion:
@@ -493,7 +465,7 @@ class GatherTaskGroup(anyio.abc.TaskGroup):
493
465
  """
494
466
  A task group that gathers results.
495
467
 
496
- AnyIO does not include support `gather`. This class extends the `TaskGroup`
468
+ AnyIO does not include `gather` support. This class extends the `TaskGroup`
497
469
  interface to allow simple gathering.
498
470
 
499
471
  See https://github.com/agronholm/anyio/issues/100
@@ -502,21 +474,31 @@ class GatherTaskGroup(anyio.abc.TaskGroup):
502
474
  """
503
475
 
504
476
  def __init__(self, task_group: anyio.abc.TaskGroup):
505
- self._results: Dict[UUID, Any] = {}
477
+ self._results: dict[UUID, Any] = {}
506
478
  # The concrete task group implementation to use
507
479
  self._task_group: anyio.abc.TaskGroup = task_group
508
480
 
509
- async def _run_and_store(self, key, fn, args):
481
+ async def _run_and_store(
482
+ self,
483
+ key: UUID,
484
+ fn: Callable[[Unpack[PosArgsT]], Awaitable[Any]],
485
+ *args: Unpack[PosArgsT],
486
+ ) -> None:
510
487
  self._results[key] = await fn(*args)
511
488
 
512
- def start_soon(self, fn, *args) -> UUID:
489
+ def start_soon( # pyright: ignore[reportIncompatibleMethodOverride]
490
+ self,
491
+ func: Callable[[Unpack[PosArgsT]], Awaitable[Any]],
492
+ *args: Unpack[PosArgsT],
493
+ name: object = None,
494
+ ) -> UUID:
513
495
  key = uuid4()
514
496
  # Put a placeholder in-case the result is retrieved earlier
515
497
  self._results[key] = GatherIncomplete
516
- self._task_group.start_soon(self._run_and_store, key, fn, args)
498
+ self._task_group.start_soon(self._run_and_store, key, func, *args, name=name)
517
499
  return key
518
500
 
519
- async def start(self, fn, *args):
501
+ async def start(self, func: object, *args: object, name: object = None) -> NoReturn:
520
502
  """
521
503
  Since `start` returns the result of `task_status.started()` but here we must
522
504
  return the key instead, we just won't support this method for now.
@@ -532,11 +514,11 @@ class GatherTaskGroup(anyio.abc.TaskGroup):
532
514
  )
533
515
  return result
534
516
 
535
- async def __aenter__(self):
517
+ async def __aenter__(self) -> Self:
536
518
  await self._task_group.__aenter__()
537
519
  return self
538
520
 
539
- async def __aexit__(self, *tb):
521
+ async def __aexit__(self, *tb: Any) -> Optional[bool]:
540
522
  try:
541
523
  retval = await self._task_group.__aexit__(*tb)
542
524
  return retval
@@ -552,14 +534,14 @@ def create_gather_task_group() -> GatherTaskGroup:
552
534
  return GatherTaskGroup(anyio.create_task_group())
553
535
 
554
536
 
555
- async def gather(*calls: Callable[[], Coroutine[Any, Any, T]]) -> List[T]:
537
+ async def gather(*calls: Callable[[], Coroutine[Any, Any, T]]) -> list[T]:
556
538
  """
557
539
  Run calls concurrently and gather their results.
558
540
 
559
541
  Unlike `asyncio.gather` this expects to receive _callables_ not _coroutines_.
560
542
  This matches `anyio` semantics.
561
543
  """
562
- keys = []
544
+ keys: list[UUID] = []
563
545
  async with create_gather_task_group() as tg:
564
546
  for call in calls:
565
547
  keys.append(tg.start_soon(call))
@@ -567,19 +549,23 @@ async def gather(*calls: Callable[[], Coroutine[Any, Any, T]]) -> List[T]:
567
549
 
568
550
 
569
551
  class LazySemaphore:
570
- def __init__(self, initial_value_func):
571
- self._semaphore = None
552
+ def __init__(self, initial_value_func: Callable[[], int]) -> None:
553
+ self._semaphore: Optional[asyncio.Semaphore] = None
572
554
  self._initial_value_func = initial_value_func
573
555
 
574
- async def __aenter__(self):
556
+ async def __aenter__(self) -> asyncio.Semaphore:
575
557
  self._initialize_semaphore()
558
+ if TYPE_CHECKING:
559
+ assert self._semaphore is not None
576
560
  await self._semaphore.__aenter__()
577
561
  return self._semaphore
578
562
 
579
- async def __aexit__(self, exc_type, exc, tb):
580
- await self._semaphore.__aexit__(exc_type, exc, tb)
563
+ async def __aexit__(self, *args: Any) -> None:
564
+ if TYPE_CHECKING:
565
+ assert self._semaphore is not None
566
+ await self._semaphore.__aexit__(*args)
581
567
 
582
- def _initialize_semaphore(self):
568
+ def _initialize_semaphore(self) -> None:
583
569
  if self._semaphore is None:
584
570
  initial_value = self._initial_value_func()
585
571
  self._semaphore = asyncio.Semaphore(initial_value)