commit: 1c6fb14f45ddccdeffe7175794c82098ba01bc49
Author: Brian Harring <ferringb <AT> gmail <DOT> com>
AuthorDate: Sat Jan 10 12:49:42 2026 +0000
Commit: Brian Harring <ferringb <AT> gmail <DOT> com>
CommitDate: Sat Jan 10 13:08:48 2026 +0000
URL:
https://gitweb.gentoo.org/proj/pkgcore/snakeoil.git/commit/?id=1c6fb14f
tools.{find_unused_exports -> imports}; renamne and use subcommands
The import ast machinery is usable for doing further __all__ analysis,
so this will become a general tool for dealing with imports.
Also fix a double reporting bug when source modules namespace is target
modules namespace.
Signed-off-by: Brian Harring <ferringb <AT> gmail.com>
.../tools/{find_unused_exports.py => imports.py} | 58 +++++++++++-----------
1 file changed, 28 insertions(+), 30 deletions(-)
diff --git a/src/snakeoil/tools/find_unused_exports.py
b/src/snakeoil/tools/imports.py
similarity index 90%
rename from src/snakeoil/tools/find_unused_exports.py
rename to src/snakeoil/tools/imports.py
index 5d834fa..4388837 100644
--- a/src/snakeoil/tools/find_unused_exports.py
+++ b/src/snakeoil/tools/imports.py
@@ -3,7 +3,6 @@
__all__ = ("main",)
-import argparse
import ast
import logging
import sys
@@ -12,12 +11,10 @@ from pathlib import Path
from textwrap import dedent
from typing import NamedTuple, Optional, Self, cast
+from snakeoil.cli import arghparse
+from snakeoil.cli.tool import Tool
from snakeoil.python_namespaces import get_submodules_of
-# Generally hard requirement- avoid relying on snakeoil here. At somepoint
this
-# should be able to be pointed right back at snakeoil for finding components
internally
-# that are unused.
-
logger = logging.getLogger(__name__)
@@ -150,14 +147,14 @@ class ImportCollector(ast.NodeVisitor):
# just rewrite into absolute pathing
base: list[str]
if node.level:
- base = self.current.qualname.split(".")
+ base = self.current.qualname.split(".") # pyright:
ignore[reportAssignmentType]
level = node.level - self.level_adjustment
if level:
base = base[:-level]
if node.module:
base.extend(node.module.split("."))
else:
- base = node.module.split(".")
+ base = node.module.split(".") # pyright:
ignore[reportOptionalMemberAccess]
for alias in node.names:
asname = self.get_asname(alias)
self.update_must_reprocess(asname)
@@ -197,16 +194,13 @@ class AttributeCollector(ast.NodeVisitor):
# terminus. This node won't have attr.
lookup.append(last)
break
- lookup.append(value.attr)
- node = node.value
+ lookup.append(value.attr) # pyright:
ignore[reportAttributeAccessIssue]
+ node = node.value # pyright:
ignore[reportAttributeAccessIssue]
except Exception as e:
print(
f"ast traversal bug in {self.current.qualname} for original
{type(node)}={node} sub-value {type(value)}={value}"
)
- import pdb
-
- pdb.set_trace()
raise e
lookup.reverse()
@@ -229,8 +223,15 @@ class AttributeCollector(ast.NodeVisitor):
mod.accessed_by[parts[0]].add(self.current)
-parser = argparse.ArgumentParser(
- __name__.rsplit(".", 1)[-1],
+parser = arghparse.ArgumentParser(
+ prog=__name__.rsplit(".", 1)[-1],
+)
+
+subparsers = parser.add_subparsers(description="commands")
+
+unused = subparsers.add_parser(
+ "unused",
+ help="tooling for finding used __all__ exports",
description=dedent(
"""\
Tool for finding potentially dead code
@@ -248,35 +249,33 @@ parser = argparse.ArgumentParser(
"""
),
)
-parser.add_argument(
- "source",
+unused.add_argument(
+ "target",
action="store",
type=str,
help="the python module to import and scan recursively, using __all__ to
find things only used within that codebase.",
)
-parser.add_argument(
- "targets", type=str, nargs="+", help="python namespaces to scan for usage."
-)
-parser.add_argument(
- "-v", action="store_true", default=False, dest="verbose", help="Increase
verbosity"
+unused.add_argument(
+ "consumers", type=str, nargs="+", help="python namespaces to scan for
usage."
)
[email protected]_main_func
def main(options, out, err) -> int:
root = ModuleImport(None, None, "")
- source_modules: list[ModuleImport] = []
+ target_modules: set[ModuleImport] = set()
ast_sources = {}
# pre-initialize the module tree of what we care about.
- for target in tuple(options.targets) + (options.source,):
+ for target in tuple(options.consumers) + (options.target,):
for module in get_submodules_of(target, include_root=True):
obj = root.create(module.__name__.split("."))
obj.alls = getattr(module, "__all__", None)
p = Path(cast(str, module.__file__))
with p.open("r") as f:
ast_sources[obj] = (p, ast.parse(f.read(), str(p)))
- if target == options.source:
- source_modules.append(obj)
+ if target == options.target:
+ target_modules.add(obj)
# collect and finalize imports, then run analysis based on attribute
access.
@@ -305,12 +304,12 @@ def main(options, out, err) -> int:
AttributeCollector(root, mod).visit(tree)
results = []
- for mod in source_modules:
+ for mod in sorted(target_modules, key=lambda x: x.qualname):
results.append(result := [mod.qualname])
if mod.alls is None:
result.append(f"{mod.qualname} has no __all__. Not analyzing")
continue
- if options.verbose:
+ if options.verbosity:
result.append("__all__ = (" + ", ".join(sorted(mod.alls)) + ")")
missing = list(sorted(set(mod.alls).difference(mod.accessed_by)))
@@ -326,7 +325,7 @@ def main(options, out, err) -> int:
first = ""
for block in sorted(results, key=lambda l: l[0]):
- if len(block) == 1 and not options.verbose:
+ if len(block) == 1 and not options.verbosity:
continue
out.write(f"{first}{block[0]}\n")
first = "\n"
@@ -341,5 +340,4 @@ def main(options, out, err) -> int:
if __name__ == "__main__":
- options = parser.parse_args()
- sys.exit(main(options, sys.stdout, sys.stderr))
+ sys.exit(Tool(parser)())