2424 strtobool ,
2525)
2626
27- from find_packages import iterate_dirnames
27+ from find_packages import is_classical_package , is_namespace_package , iter_packages
2828
2929REAMDE_BADGES_START_DELIMITER = "<!--- Top of README Badges (automated) --->"
3030REAMDE_BADGES_END_DELIMITER = "<!--- End of README Badges (automated) --->"
@@ -229,49 +229,48 @@ def __init__(
229229 raise NotADirectoryError (root )
230230 self .gha_input = gha_input
231231 self .root = root .resolve ()
232- self ._pkg_paths = self ._get_package_paths (self .gha_input .exclude_dirs )
233- self .packages = [p .name for p in self ._pkg_paths ]
232+ self .package_paths = self ._get_package_paths ()
234233 self .readme_path = self ._get_readme_path ()
235234
236235 self .check_no_version_dunders () # do now so we don't forget to
237236
238- def _get_package_paths (self , dirs_exclude : list [ str ] ) -> list [Path ]:
237+ def _get_package_paths (self ) -> list [Path ]:
239238 """Find the package path(s)."""
240-
241- if not ( available_pkgs := list ( iterate_dirnames ( self . root , dirs_exclude ))) :
239+ found_pkgs = list ( iter_packages ( self . root , self . gha_input . exclude_dirs ))
240+ if not found_pkgs :
242241 raise _log_error_then_get_exception (
243242 f"No package found in '{ self .root } '. Are you missing an __init__.py?"
244243 )
245244
246245 # check the pyproject.toml: package_dirs
247246 if self .gha_input .package_dirs :
248- if not_ins := [
249- p for p in self .gha_input .package_dirs if p not in available_pkgs
247+ if missings := [
248+ p for p in self .gha_input .package_dirs if p not in found_pkgs
250249 ]:
251- if len (not_ins ) == 1 :
250+ if len (missings ) == 1 :
252251 raise _log_error_then_get_exception (
253252 f"Package directory not found: "
254- f"{ not_ins [0 ]} (defined in pyproject.toml). "
253+ f"{ missings [0 ]} (defined in pyproject.toml). "
255254 f"Is the directory missing an __init__.py?"
256255 )
257256 raise _log_error_then_get_exception (
258257 f"Package directories not found: "
259- f"{ ', ' .join (not_ins )} (defined in pyproject.toml). "
258+ f"{ ', ' .join (missings )} (defined in pyproject.toml). "
260259 f"Are the directories missing __init__.py files?"
261260 )
262-
263- return [self .root / p for p in self .gha_input .package_dirs ]
261+ else :
262+ return [self .root / p for p in self .gha_input .package_dirs ]
264263 # use the auto-detected package (if there's ONE)
265264 else :
266- if len (available_pkgs ) > 1 :
265+ if len (found_pkgs ) > 1 :
267266 raise _log_error_then_get_exception (
268- f"More than one package found in '{ self .root } ': { ', ' .join (available_pkgs )} . "
267+ f"More than one package found in '{ self .root } ': { ', ' .join (found_pkgs )} . "
269268 f"Either "
270269 f"[1] list *all* your desired packages in your pyproject.toml's 'package_dirs', "
271270 f"[2] remove the extra __init__.py file(s), "
272271 f"or [3] list which packages to ignore in your GitHub Action step's 'with.exclude-dirs'."
273272 )
274- return [self .root / available_pkgs [0 ]]
273+ return [self .root / found_pkgs [0 ]]
275274
276275 def check_no_version_dunders (self ) -> None :
277276 """Check that no modules' __init__.py define a __version__ attribute."""
@@ -288,7 +287,7 @@ def commenter(match):
288287 git_update_these = []
289288
290289 # detect
291- for pkg in self ._pkg_paths :
290+ for pkg in self .package_paths :
292291 init_py = pkg / "__init__.py"
293292
294293 # use a regex subn to detect __version__ and do a replace at same time
@@ -477,17 +476,15 @@ def __init__(
477476 toml_dict ["tool" ]["setuptools" ] = {}
478477 toml_dict ["tool" ]["setuptools" ].update (
479478 {
480- "packages" : {
481- "find" : self ._tool_setuptools_packages_find (gha_input ),
482- },
479+ "packages" : self ._tool_setuptools_packages (ffile ),
483480 "package-data" : {
484481 ** toml_dict ["tool" ].get ("setuptools" , {}).get ("package-data" , {}),
485482 "*" : self ._tool_setuptools_packagedata_star (toml_dict ),
486483 },
487484 }
488485 )
489486 self ._inline_dont_change_this_comment (
490- toml_dict ["tool" ]["setuptools" ]["packages" ][ "find" ]
487+ toml_dict ["tool" ]["setuptools" ]["packages" ]
491488 )
492489 self ._inline_dont_change_this_comment (
493490 toml_dict ["tool" ]["setuptools" ]["package-data" ]["*" ]
@@ -529,7 +526,9 @@ def insert_packaging_attributes(
529526 if gha_input .mode != "PACKAGING" :
530527 raise RuntimeError (f"cannot add 'PACKAGING' attrs for { gha_input .mode = } " )
531528
532- toml_project ["name" ] = "_" .join (ffile .packages ).replace ("_" , "-" )
529+ toml_project ["name" ] = "-" .join (
530+ p .name .replace ("_" , "-" ) for p in ffile .package_paths
531+ )
533532 PyProjectTomlBuilder ._inline_dont_change_this_comment (toml_project ["name" ])
534533
535534 toml_project ["requires-python" ] = gha_input .get_requires_python ()
@@ -638,18 +637,27 @@ def _validate_repo_initial_state(toml_dict: TOMLDocumentTypeHint) -> None:
638637 # <none>
639638
640639 @staticmethod
641- def _tool_setuptools_packages_find (gha_input : GHAInput ) -> dict [str , Any ]:
642- # only allow these...
643- if gha_input .package_dirs :
644- return {
645- "include" : gha_input .package_dirs
646- + [f"{ p } .*" for p in gha_input .package_dirs ]
647- }
648- # disallow these...
649- dicto : dict [str , Any ] = {"namespaces" : False }
650- if gha_input .exclude_dirs :
651- dicto .update ({"exclude" : gha_input .exclude_dirs })
652- return dicto
640+ def _tool_setuptools_packages (ffile : FromFiles ) -> list [str ]:
641+ """
642+ Recursively collect package and subpackage names from the given base paths.
643+ Includes classic packages (__init__.py) and PEP 420 namespaces (dirs with .py files).
644+ """
645+ names : set [str ] = set ()
646+
647+ for pkg in ffile .package_paths : # each is a Path
648+ names .add (pkg .name )
649+
650+ # Walk all subdirs
651+ for path in pkg .rglob ("*" ):
652+ if not path .is_dir ():
653+ continue
654+
655+ if is_classical_package (path ) or is_namespace_package (path ):
656+ rel = str (path .relative_to (pkg )) # e.g., "api/utils"
657+ if rel : # skip the parent
658+ names .add (f"{ pkg .name } .{ rel .replace ('/' , '.' )} " )
659+
660+ return sorted (names )
653661
654662 @staticmethod
655663 def _tool_setuptools_packagedata_star (toml_dict : TOMLDocumentTypeHint ) -> list [str ]:
@@ -717,6 +725,7 @@ def write_toml(
717725 optional_deps = toml_dict .get ("project" , {}).get ("optional-dependencies" , {})
718726 for key in optional_deps :
719727 set_multiline_array (optional_deps , key , sort = True )
728+ set_multiline_array (toml_dict , "tool" , "setuptools" , "packages" , sort = True )
720729
721730 # remove sections that used to be auto-added but are now not needed
722731 # -> [tool.semantic_release], [tool.semantic_release.commit_parser_options]
0 commit comments