@@ -145,16 +145,77 @@ def detect_packages_from_files( # noqa: C901, PLR0912, PLR0915
145145 detected : set [str ] = set ()
146146 parent_dirs : list [Path ] = []
147147 seen_parents : set [Path ] = set ()
148+ # Track which packages were detected via source_bases
149+ # (for namespace package detection)
150+ detected_via_source_bases : set [str ] = set ()
148151
149152 # Detect packages from files
150153 for file_path in file_paths :
154+ file_path_resolved = file_path .resolve ()
151155 pkg_root = ApatheticUtils_Internal_Modules ._find_package_root_for_file (
152156 file_path , source_bases = source_bases
153157 )
154158 if pkg_root :
159+ # Check if this package was detected via source_bases
160+ # (by checking if file is under any source_base and no __init__.py)
161+ detected_via_sb = False
162+ if source_bases :
163+ # Check if file is under a source_base
164+ for base_str in source_bases :
165+ base_path = Path (base_str ).resolve ()
166+ try :
167+ rel_path = file_path_resolved .relative_to (base_path )
168+ # If file is in a subdirectory of base, check __init__.py
169+ if (
170+ len (rel_path .parts ) > 1
171+ and not (pkg_root / "__init__.py" ).exists ()
172+ ):
173+ # No __init__.py, so detected via source_bases
174+ detected_via_sb = True
175+ break
176+ except ValueError :
177+ continue
178+
155179 # Extract package name from directory name
156180 pkg_name = pkg_root .name
157181 detected .add (pkg_name )
182+ if detected_via_sb :
183+ detected_via_source_bases .add (pkg_name )
184+
185+ # For source_bases, also detect nested packages (all directory levels)
186+ if detected_via_sb and source_bases :
187+ # Find which base this file is under
188+ for base_str in source_bases :
189+ base_path = Path (base_str ).resolve ()
190+ try :
191+ rel_path = file_path_resolved .relative_to (base_path )
192+ # Detect all directory levels between base and file
193+ # (excluding the file itself and the base)
194+ # More than base + first level + file
195+ MIN_NESTED_PARTS = 3
196+ if len (rel_path .parts ) >= MIN_NESTED_PARTS :
197+ # Walk from base to file, detecting intermediate dirs
198+ current = base_path
199+ for part in rel_path .parts [:- 1 ]: # Exclude filename
200+ current = current / part
201+ if current .is_dir () and current != pkg_root :
202+ nested_pkg_name = current .name
203+ if (
204+ nested_pkg_name
205+ and nested_pkg_name != package_name
206+ ):
207+ detected .add (nested_pkg_name )
208+ detected_via_source_bases .add (
209+ nested_pkg_name
210+ )
211+ logger .trace (
212+ "[PKG_DETECT] Detected nested package "
213+ "%s from %s" ,
214+ nested_pkg_name ,
215+ file_path ,
216+ )
217+ except ValueError :
218+ continue
158219
159220 # Extract parent directory (module base)
160221 parent_dir = pkg_root .parent .resolve ()
@@ -165,14 +226,16 @@ def detect_packages_from_files( # noqa: C901, PLR0912, PLR0915
165226 parent_dirs .append (parent_dir )
166227
167228 logger .trace (
168- "[PKG_DETECT] Detected package %s from %s (root: %s, parent: %s)" ,
229+ "[PKG_DETECT] Detected package %s from %s "
230+ "(root: %s, parent: %s, via_sb=%s)" ,
169231 pkg_name ,
170232 file_path ,
171233 pkg_root ,
172234 parent_dir ,
235+ detected_via_sb ,
173236 )
174237
175- # Also detect directories in source_bases as packages if they contain
238+ # Also detect directories as namespace packages if they contain
176239 # subdirectories that are packages (namespace packages)
177240 # This must happen BEFORE adding package_name to detected, so we can check
178241 # if base_name == package_name correctly
@@ -195,61 +258,72 @@ def detect_packages_from_files( # noqa: C901, PLR0912, PLR0915
195258 # No common root, use first file's parent
196259 common_root = file_paths [0 ].parent
197260 break
261+
262+ # Build set of source_bases paths for quick lookup
263+ source_bases_paths : set [Path ] = set ()
198264 if source_bases :
199265 for base_str in source_bases :
200266 base_path = Path (base_str ).resolve ()
201- if not base_path .exists () or not base_path .is_dir ():
202- continue
203- # Check if this base contains any detected packages as direct children
204- base_name = base_path .name
205- # Skip if base is filesystem root, empty name, already detected,
206- # is package_name, or is the common root of all files
207- if (
208- not base_name
209- or base_name in detected
210- or base_name == package_name
211- or base_path == base_path .parent # filesystem root
212- or (common_root and base_path == common_root .resolve ())
213- ):
214- logger .trace (
215- "[PKG_DETECT] Skipping base %s: name=%s, in_detected=%s, "
216- "is_package_name=%s, is_common_root=%s" ,
217- base_path ,
218- base_name ,
219- base_name in detected ,
220- base_name == package_name ,
221- common_root and base_path == common_root .resolve (),
222- )
223- continue
224- # Check if any detected package has this base as its parent
225- for file_path in file_paths :
226- pkg_root = (
227- ApatheticUtils_Internal_Modules ._find_package_root_for_file (
228- file_path , source_bases = source_bases
229- )
230- )
231- if pkg_root :
232- pkg_parent = pkg_root .parent .resolve ()
267+ if base_path .exists () and base_path .is_dir ():
268+ source_bases_paths .add (base_path )
269+
270+ # Check all detected packages' parent directories to see if they should
271+ # be detected as namespace packages
272+ # Only detect namespace packages when using source_bases
273+ if source_bases :
274+ checked_parents : set [Path ] = set ()
275+ for file_path in file_paths :
276+ file_path_resolved = file_path .resolve ()
277+ pkg_root = ApatheticUtils_Internal_Modules ._find_package_root_for_file (
278+ file_path , source_bases = source_bases
279+ )
280+ if pkg_root :
281+ pkg_name = pkg_root .name
282+ # Only check parents if this package was detected via source_bases
283+ if pkg_name not in detected_via_source_bases :
284+ continue
285+
286+ pkg_parent = pkg_root .parent .resolve ()
287+ # Skip if we've already checked this parent
288+ if pkg_parent in checked_parents :
289+ continue
290+ checked_parents .add (pkg_parent )
291+
292+ # Skip if parent is filesystem root
293+ if pkg_parent == pkg_parent .parent :
294+ continue
295+
296+ parent_name = pkg_parent .name
297+ # Skip if empty name, already detected, is package_name,
298+ # is the common root, or is in source_bases
299+ if (
300+ not parent_name
301+ or parent_name in detected
302+ or parent_name == package_name
303+ or (common_root and pkg_parent == common_root .resolve ())
304+ or pkg_parent in source_bases_paths
305+ ):
233306 logger .trace (
234- "[PKG_DETECT] Checking base: %s (base_path=%s), "
235- "pkg_root=%s, pkg_parent=%s, match=%s" ,
236- base_name ,
237- base_path ,
238- pkg_root ,
307+ "[PKG_DETECT] Skipping parent %s: name=%s, in_detected=%s, "
308+ "is_package_name=%s, is_common_root=%s, in_source_bases=%s" ,
239309 pkg_parent ,
240- pkg_parent == base_path ,
310+ parent_name ,
311+ parent_name in detected ,
312+ parent_name == package_name ,
313+ common_root and pkg_parent == common_root .resolve (),
314+ pkg_parent in source_bases_paths ,
241315 )
242- if pkg_parent == base_path :
243- # This base contains a detected package,
244- # so it's also a package
245- detected .add (base_name )
246- logger . trace (
247- "[PKG_DETECT] Detected base directory as package: %s "
248- "(contains package: %s)" ,
249- base_name ,
250- pkg_root . name ,
251- )
252- break
316+ continue
317+
318+ # This parent contains a detected package, so it's also a package
319+ detected .add (parent_name )
320+ detected_via_source_bases . add ( parent_name )
321+ logger . trace (
322+ "[PKG_DETECT] Detected parent directory as namespace package: "
323+ "%s (contains package: %s)" ,
324+ parent_name ,
325+ pkg_root . name ,
326+ )
253327
254328 # Always include configured package (for fallback and multi-package scenarios)
255329 detected .add (package_name )
0 commit comments