|
4 | 4 |
|
5 | 5 | import functools |
6 | 6 | import inspect |
| 7 | +import sys |
7 | 8 | from collections import defaultdict |
8 | | -from contextlib import suppress |
9 | 9 | from types import BuiltinFunctionType |
10 | 10 | from typing import TYPE_CHECKING |
11 | 11 | from typing import Any |
|
44 | 44 | """ |
45 | 45 |
|
46 | 46 |
|
47 | | -def task( # noqa: PLR0913 |
| 47 | +def task( # noqa: PLR0913, C901 |
48 | 48 | name: str | None = None, |
49 | 49 | *, |
50 | 50 | after: str | Callable[..., Any] | list[Callable[..., Any]] | None = None, |
51 | 51 | is_generator: bool = False, |
52 | 52 | id: str | None = None, # noqa: A002 |
53 | 53 | kwargs: dict[Any, Any] | None = None, |
54 | 54 | produces: Any | None = None, |
| 55 | + _caller_locals: dict[str, Any] | None = None, |
55 | 56 | ) -> Callable[..., Callable[..., Any]]: |
56 | 57 | """Decorate a task function. |
57 | 58 |
|
@@ -109,6 +110,11 @@ def create_text_file() -> Annotated[str, Path("file.txt")]: |
109 | 110 | return "Hello, World!" |
110 | 111 |
|
111 | 112 | """ |
| 113 | + # Capture the caller's frame locals for deferred annotation evaluation in Python |
| 114 | + # 3.14+. This must be done here (not in wrapper) to get the correct scope when |
| 115 | + # @task is used without parentheses. |
| 116 | + if _caller_locals is None: |
| 117 | + _caller_locals = sys._getframe(1).f_locals.copy() |
112 | 118 |
|
113 | 119 | def wrapper(func: Callable[..., Any]) -> Callable[..., Any]: |
114 | 120 | # Omits frame when a builtin function is wrapped. |
@@ -144,7 +150,7 @@ def wrapper(func: Callable[..., Any]) -> Callable[..., Any]: |
144 | 150 | parsed_name = _parse_name(unwrapped, name) |
145 | 151 | parsed_after = _parse_after(after) |
146 | 152 |
|
147 | | - annotation_locals = _snapshot_annotation_locals(unwrapped) |
| 153 | + annotation_locals = _snapshot_annotation_locals(_caller_locals) |
148 | 154 |
|
149 | 155 | if hasattr(unwrapped, "pytask_meta"): |
150 | 156 | unwrapped.pytask_meta.after = parsed_after |
@@ -182,7 +188,7 @@ def wrapper(func: Callable[..., Any]) -> Callable[..., Any]: |
182 | 188 | # In case the decorator is used without parentheses, wrap the function which is |
183 | 189 | # passed as the first argument with the default arguments. |
184 | 190 | if is_task_function(name) and kwargs is None: |
185 | | - return task()(name) |
| 191 | + return task(_caller_locals=_caller_locals)(name) |
186 | 192 | return wrapper |
187 | 193 |
|
188 | 194 |
|
@@ -308,21 +314,20 @@ def parse_keyword_arguments_from_signature_defaults( |
308 | 314 | return kwargs |
309 | 315 |
|
310 | 316 |
|
311 | | -def _snapshot_annotation_locals(func: Callable[..., Any]) -> dict[str, Any] | None: |
312 | | - """Capture the values of free variables at decoration time for annotations.""" |
313 | | - while isinstance(func, functools.partial): |
314 | | - func = func.func |
| 317 | +def _snapshot_annotation_locals( |
| 318 | + caller_locals: dict[str, Any] | None, |
| 319 | +) -> dict[str, Any] | None: |
| 320 | + """Capture caller's frame locals at decoration time for deferred annotation eval. |
315 | 321 |
|
316 | | - closure = getattr(func, "__closure__", None) |
317 | | - if not closure: |
318 | | - return None |
| 322 | + This function captures variables that may be referenced in type annotations but |
| 323 | + won't be available when annotations are evaluated later (e.g., loop variables in |
| 324 | + task generators under Python 3.14's PEP 649 deferred annotations). |
319 | 325 |
|
320 | | - snapshot = {} |
321 | | - for name, cell in zip(func.__code__.co_freevars, closure, strict=False): |
322 | | - with suppress(ValueError): |
323 | | - snapshot[name] = cell.cell_contents |
| 326 | + We capture the caller's frame locals - variables in the scope where @task is |
| 327 | + applied (e.g., loop variables like `path` that are only referenced in annotations). |
324 | 328 |
|
325 | | - return snapshot or None |
| 329 | + """ |
| 330 | + return caller_locals.copy() if caller_locals else None |
326 | 331 |
|
327 | 332 |
|
328 | 333 | def _generate_ids_for_tasks( |
|
0 commit comments