66import time
77import warnings
88from concurrent .futures import Future
9+ from pathlib import Path
10+ from types import ModuleType
911from types import TracebackType
1012from typing import Any
1113from typing import Callable
12- from typing import List
1314
14- import attr
1515import cloudpickle
16+ from attrs import define
17+ from attrs import field
1618from pytask import console
1719from pytask import ExecutionReport
1820from pytask import get_marks
@@ -56,6 +58,8 @@ def pytask_execute_build(session: Session) -> bool | None: # noqa: C901, PLR091
5658 3. Process all reports and report the result on the command line.
5759
5860 """
61+ __tracebackhide__ = True
62+
5963 if session .config ["n_workers" ] > 1 :
6064 reports = session .execution_reports
6165 running_tasks : dict [str , Future [Any ]] = {}
@@ -191,7 +195,7 @@ def pytask_execute_task(session: Session, task: PTask) -> Future[Any] | None:
191195 # the child process. We have to register the module as dynamic again, so
192196 # that cloudpickle will pickle it with the function. See cloudpickle#417,
193197 # pytask#373 and pytask#374.
194- task_module = inspect . getmodule (task .function )
198+ task_module = _get_module (task .function , getattr ( task , "path" , None ) )
195199 cloudpickle .register_pickle_by_value (task_module )
196200
197201 return session .config ["_parallel_executor" ].submit (
@@ -344,7 +348,7 @@ def _create_kwargs_for_task(task: PTask) -> dict[str, PyTree[Any]]:
344348 return kwargs
345349
346350
347- @attr . s (kw_only = True )
351+ @define (kw_only = True )
348352class _Sleeper :
349353 """A sleeper that always sleeps a bit and up to 1 second if you don't wake it up.
350354
@@ -353,8 +357,8 @@ class _Sleeper:
353357
354358 """
355359
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
358362
359363 def reset (self ) -> None :
360364 self .timing_idx = 0
@@ -365,3 +369,21 @@ def increment(self) -> None:
365369
366370 def sleep (self ) -> None :
367371 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