2121from _pytask .console import create_summary_panel
2222from _pytask .console import get_file
2323from _pytask .exceptions import CollectionError
24+ from _pytask .exceptions import NodeNotCollectedError
2425from _pytask .mark_utils import get_all_marks
2526from _pytask .mark_utils import has_mark
2627from _pytask .node_protocols import PNode
3738from _pytask .path import shorten_path
3839from _pytask .reports import CollectionReport
3940from _pytask .shared import find_duplicates
41+ from _pytask .task_utils import COLLECTED_TASKS
4042from _pytask .task_utils import task as task_decorator
4143from _pytask .typing import is_task_function
4244from rich .text import Text
@@ -61,6 +63,7 @@ def pytask_collect(session: Session) -> bool:
6163
6264 _collect_from_paths (session )
6365 _collect_from_tasks (session )
66+ _collect_not_collected_tasks (session )
6467
6568 session .tasks .extend (
6669 i .node
@@ -108,6 +111,9 @@ def _collect_from_tasks(session: Session) -> None:
108111 path = get_file (raw_task )
109112 name = raw_task .pytask_meta .name
110113
114+ if has_mark (raw_task , "task" ):
115+ COLLECTED_TASKS [path ].remove (raw_task )
116+
111117 # When a task is not a callable, it can be anything or a PTask. Set arbitrary
112118 # values and it will pass without errors and not collected.
113119 else :
@@ -126,6 +132,45 @@ def _collect_from_tasks(session: Session) -> None:
126132 session .collection_reports .append (report )
127133
128134
135+ _FAILED_COLLECTING_TASK = """\
136+ Failed to collect task '{name}'{path_desc}.
137+
138+ This can happen when the task function is defined in another module, imported to a \
139+ task module and wrapped with the '@task' decorator.
140+
141+ To collect this task correctly, wrap the imported function in a lambda expression like
142+
143+ task(...)(lambda **x: imported_function(**x)).
144+ """
145+
146+
147+ def _collect_not_collected_tasks (session : Session ) -> None :
148+ """Collect tasks that are not collected yet and create failed reports."""
149+ for path in list (COLLECTED_TASKS ):
150+ tasks = COLLECTED_TASKS .pop (path )
151+ for task in tasks :
152+ name = task .pytask_meta .name # type: ignore[attr-defined]
153+ node : PTask
154+ if path :
155+ node = Task (base_name = name , path = path , function = task )
156+ path_desc = f" in '{ path } '"
157+ else :
158+ node = TaskWithoutPath (name = name , function = task )
159+ path_desc = ""
160+ report = CollectionReport (
161+ outcome = CollectionOutcome .FAIL ,
162+ node = node ,
163+ exc_info = (
164+ NodeNotCollectedError ,
165+ NodeNotCollectedError (
166+ _FAILED_COLLECTING_TASK .format (name = name , path_desc = path_desc )
167+ ),
168+ None ,
169+ ),
170+ )
171+ session .collection_reports .append (report )
172+
173+
129174@hookimpl
130175def pytask_ignore_collect (path : Path , config : dict [str , Any ]) -> bool :
131176 """Ignore a path during the collection."""
0 commit comments