6
6
import time
7
7
import warnings
8
8
from concurrent .futures import Future
9
+ from pathlib import Path
10
+ from types import ModuleType
9
11
from types import TracebackType
10
12
from typing import Any
11
13
from typing import Callable
12
- from typing import List
13
14
14
- import attr
15
15
import cloudpickle
16
+ from attrs import define
17
+ from attrs import field
16
18
from pytask import console
17
19
from pytask import ExecutionReport
18
20
from pytask import get_marks
@@ -56,6 +58,8 @@ def pytask_execute_build(session: Session) -> bool | None: # noqa: C901, PLR091
56
58
3. Process all reports and report the result on the command line.
57
59
58
60
"""
61
+ __tracebackhide__ = True
62
+
59
63
if session .config ["n_workers" ] > 1 :
60
64
reports = session .execution_reports
61
65
running_tasks : dict [str , Future [Any ]] = {}
@@ -191,7 +195,7 @@ def pytask_execute_task(session: Session, task: PTask) -> Future[Any] | None:
191
195
# the child process. We have to register the module as dynamic again, so
192
196
# that cloudpickle will pickle it with the function. See cloudpickle#417,
193
197
# pytask#373 and pytask#374.
194
- task_module = inspect . getmodule (task .function )
198
+ task_module = _get_module (task .function , getattr ( task , "path" , None ) )
195
199
cloudpickle .register_pickle_by_value (task_module )
196
200
197
201
return session .config ["_parallel_executor" ].submit (
@@ -344,7 +348,7 @@ def _create_kwargs_for_task(task: PTask) -> dict[str, PyTree[Any]]:
344
348
return kwargs
345
349
346
350
347
- @attr . s (kw_only = True )
351
+ @define (kw_only = True )
348
352
class _Sleeper :
349
353
"""A sleeper that always sleeps a bit and up to 1 second if you don't wake it up.
350
354
@@ -353,8 +357,8 @@ class _Sleeper:
353
357
354
358
"""
355
359
356
- timings = attr . ib ( type = List [float ], default = [(i / 10 ) ** 2 for i in range (1 , 11 )])
357
- timing_idx = attr . ib ( type = int , default = 0 )
360
+ timings : list [float ] = field ( default = [(i / 10 ) ** 2 for i in range (1 , 11 )])
361
+ timing_idx : int = 0
358
362
359
363
def reset (self ) -> None :
360
364
self .timing_idx = 0
@@ -365,3 +369,21 @@ def increment(self) -> None:
365
369
366
370
def sleep (self ) -> None :
367
371
time .sleep (self .timings [self .timing_idx ])
372
+
373
+
374
+ def _get_module (func : Callable [..., Any ], path : Path ) -> ModuleType :
375
+ """Get the module of a python function.
376
+
377
+ For Python <3.10, functools.partial does not set a `__module__` attribute which is
378
+ why ``inspect.getmodule`` returns ``None`` and ``cloudpickle.pickle_by_value``
379
+ fails. In later versions, ``functools`` is returned and everything seems to work
380
+ fine.
381
+
382
+ Therefore, we use the path from the task module to aid the search which works for
383
+ Python <3.10.
384
+
385
+ We do not unwrap the partialed function with ``func.func``, since pytask in general
386
+ does not really support ``functools.partial``. Instead, use ``@task(kwargs=...)``.
387
+
388
+ """
389
+ return inspect .getmodule (func , path .as_posix ())
0 commit comments