Skip to content

Commit a8daad7

Browse files
authored
Use tool.setuptools.packages: include subpackages [minor] (#81)
1 parent 528de20 commit a8daad7

File tree

4 files changed

+191
-80
lines changed

4 files changed

+191
-80
lines changed

action.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,9 +308,9 @@ runs:
308308
from pathlib import Path
309309
310310
sys.path.append(os.path.abspath("${{ github.action_path }}"))
311-
from find_packages import iterate_dirnames
311+
from find_packages import iter_packages
312312
313-
for pkg_dname in iterate_dirnames(Path("."), "${{ inputs.exclude_dirs }}".split()):
313+
for pkg_dname in iter_packages(Path("."), "${{ inputs.exclude_dirs }}".split()):
314314
print(pkg_dname)
315315
fpath = Path(pkg_dname) / "py.typed"
316316
print(fpath)

find_packages.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import Iterator
55

66

7-
def iterate_dirnames(
7+
def iter_packages(
88
root_dir: Path,
99
dirs_exclude: list[str] | None = None,
1010
) -> Iterator[str]:
@@ -17,5 +17,21 @@ def iterate_dirnames(
1717
for directory in [p for p in root_dir.iterdir() if p.is_dir()]:
1818
if directory.name in dirs_exclude:
1919
continue
20-
if "__init__.py" in [p.name for p in directory.iterdir()]:
20+
if is_classical_package(directory):
2121
yield directory.name
22+
23+
24+
def is_classical_package(path: Path) -> bool:
25+
"""Return True if the path is a classical package (has __init__.py)."""
26+
return path.is_dir() and (path / "__init__.py").exists()
27+
28+
29+
def is_namespace_package(path: Path) -> bool:
30+
"""
31+
Return True if the path is an implicit namespace package (PEP 420):
32+
directory with no __init__.py that contains at least one .py file directly.
33+
"""
34+
if not path.is_dir() or (path / "__init__.py").exists():
35+
return False
36+
37+
return any(p.suffix == ".py" for p in path.iterdir() if p.is_file())

pyproject_toml_builder.py

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
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

2929
REAMDE_BADGES_START_DELIMITER = "<!--- Top of README Badges (automated) --->"
3030
REAMDE_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

Comments
 (0)