11"""Implement functionality to collect tasks."""
22import importlib
33import inspect
4+ import os
45import sys
56import time
67from pathlib import Path
78from typing import Generator
89from typing import List
910
1011from _pytask .config import hookimpl
12+ from _pytask .config import IS_FILE_SYSTEM_CASE_SENSITIVE
1113from _pytask .console import console
1214from _pytask .enums import ColorCode
1315from _pytask .exceptions import CollectionError
1416from _pytask .mark import has_marker
17+ from _pytask .nodes import create_task_name
1518from _pytask .nodes import FilePathNode
1619from _pytask .nodes import PythonFunctionTask
1720from _pytask .nodes import reduce_node_name
21+ from _pytask .path import find_case_sensitive_path
1822from _pytask .report import CollectionReport
1923from rich .traceback import Traceback
2024
@@ -126,9 +130,8 @@ def pytask_collect_task_protocol(session, path, name, obj):
126130 return CollectionReport .from_node (task )
127131
128132 except Exception :
129- return CollectionReport .from_exception (
130- exc_info = sys .exc_info (), node = locals ().get ("task" )
131- )
133+ task = PythonFunctionTask (name , create_task_name (path , name ), path , None )
134+ return CollectionReport .from_exception (exc_info = sys .exc_info (), node = task )
132135
133136
134137@hookimpl (trylast = True )
@@ -146,19 +149,29 @@ def pytask_collect_task(session, path, name, obj):
146149 )
147150
148151
152+ _TEMPLATE_ERROR = (
153+ "The provided path of the dependency/product in the marker is {}, but the path of "
154+ "the file on disk is {}. Case-sensitive file systems would raise an error.\n \n "
155+ "Please, align the names to ensure reproducibility on case-sensitive file systems "
156+ "(often Linux or macOS) or disable this error with 'check_casing_of_paths = false'."
157+ )
158+
159+
149160@hookimpl (trylast = True )
150- def pytask_collect_node (path , node ):
161+ def pytask_collect_node (session , path , node ):
151162 """Collect a node of a task as a :class:`pytask.nodes.FilePathNode`.
152163
153164 Strings are assumed to be paths. This might be a strict assumption, but since this
154- hook is attempted at last and possible errors will be shown, it is reasonable and
165+ hook is executed at last and possible errors will be shown, it seems reasonable and
155166 unproblematic.
156167
157168 ``trylast=True`` might be necessary if other plugins try to parse strings themselves
158169 like a plugin for downloading files which depends on URLs given as strings.
159170
160171 Parameters
161172 ----------
173+ session : _pytask.session.Session
174+ The session.
162175 path : Union[str, pathlib.Path]
163176 The path to file where the task and node are specified.
164177 node : Union[str, pathlib.Path]
@@ -170,7 +183,19 @@ def pytask_collect_node(path, node):
170183 node = Path (node )
171184 if isinstance (node , Path ):
172185 if not node .is_absolute ():
173- node = path .parent .joinpath (node )
186+ # ``normpath`` removes ``../`` from the path which is necessary for the
187+ # casing check which will fail since ``.resolves()`` also normalizes a path.
188+ node = Path (os .path .normpath (path .parent .joinpath (node )))
189+
190+ if (
191+ not IS_FILE_SYSTEM_CASE_SENSITIVE
192+ and session .config ["check_casing_of_paths" ]
193+ and sys .platform == "win32"
194+ ):
195+ case_sensitive_path = find_case_sensitive_path (node , "win32" )
196+ if str (node ) != str (case_sensitive_path ):
197+ raise Exception (_TEMPLATE_ERROR .format (node , case_sensitive_path ))
198+
174199 return FilePathNode .from_path (node )
175200
176201
0 commit comments