Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Support --exclude option to deps subcommand to exclude dependencies
  • Loading branch information
moreal authored and youknowone committed Jan 22, 2026
commit 923ed8ef5a395d94277bbcb32ae3e65555a37577
4 changes: 4 additions & 0 deletions scripts/update_lib/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,13 +605,15 @@ def find_tests_importing_module(
module_name: str,
lib_prefix: str = "Lib",
include_transitive: bool = True,
exclude_imports: frozenset[str] = frozenset(),
) -> frozenset[pathlib.Path]:
"""Find all test files that import the given module (directly or transitively).

Args:
module_name: Module to search for (e.g., "datetime")
lib_prefix: RustPython Lib directory (default: "Lib")
include_transitive: Whether to include transitive dependencies
exclude_imports: Modules to exclude from test file imports when checking

Returns:
Frozenset of test file paths that depend on this module
Expand Down Expand Up @@ -643,6 +645,8 @@ def find_tests_importing_module(
continue

imports = parse_lib_imports(content)
# Remove excluded modules from imports
imports = imports - exclude_imports
# Check if any target module is imported
if imports & target_modules:
result.add(test_file)
Expand Down
18 changes: 15 additions & 3 deletions scripts/update_lib/show_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ def format_deps(
max_depth: int = 10,
_visited: set[str] | None = None,
show_impact: bool = False,
exclude_imports: frozenset[str] = frozenset(),
) -> list[str]:
"""Format all dependency information for a module.

Expand All @@ -156,6 +157,7 @@ def format_deps(
max_depth: Maximum recursion depth
_visited: Shared visited set for deduplication across modules
show_impact: Whether to show reverse dependencies (tests that import this module)
exclude_imports: Modules to exclude from impact analysis

Returns:
List of formatted lines
Expand Down Expand Up @@ -200,7 +202,7 @@ def format_deps(

# Show impact (reverse dependencies) if requested
if show_impact:
impacted_tests = find_tests_importing_module(name, lib_prefix)
impacted_tests = find_tests_importing_module(name, lib_prefix, exclude_imports=exclude_imports)
transitive_importers = get_transitive_imports(name, lib_prefix)

if impacted_tests:
Expand All @@ -212,6 +214,8 @@ def format_deps(
from update_lib.deps import parse_lib_imports

test_imports = parse_lib_imports(test_content)
# Remove excluded modules from display (consistent with matching)
test_imports = test_imports - exclude_imports
if name in test_imports:
lines.append(f" - {test_path.name} (direct)")
else:
Expand All @@ -234,6 +238,7 @@ def show_deps(
lib_prefix: str = "Lib",
max_depth: int = 10,
show_impact: bool = False,
exclude_imports: frozenset[str] = frozenset(),
) -> None:
"""Show all dependency information for modules."""
# Expand "all" to all module names
Expand All @@ -251,7 +256,7 @@ def show_deps(
if i > 0:
print() # blank line between modules
for line in format_deps(
name, cpython_prefix, lib_prefix, max_depth, visited, show_impact
name, cpython_prefix, lib_prefix, max_depth, visited, show_impact, exclude_imports
):
print(line)

Expand Down Expand Up @@ -287,11 +292,18 @@ def main(argv: list[str] | None = None) -> int:
action="store_true",
help="Show tests that import this module (reverse dependencies)",
)
parser.add_argument(
"--exclude",
action="append",
default=[],
help="Modules to exclude from impact analysis (can be repeated: --exclude unittest --exclude doctest)",
)

args = parser.parse_args(argv)

try:
show_deps(args.names, args.cpython, args.lib, args.depth, args.impact)
exclude_imports = frozenset(args.exclude)
show_deps(args.names, args.cpython, args.lib, args.depth, args.impact, exclude_imports)
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
Expand Down
101 changes: 101 additions & 0 deletions scripts/update_lib/tests/test_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,5 +552,106 @@ def test_empty_when_no_importers(self):
self.assertEqual(result, frozenset())


class TestFindTestsImportingModuleExclude(unittest.TestCase):
"""Tests for exclude_imports parameter."""

def test_exclude_single_module(self):
"""Test excluding a single module from import analysis."""
# Given:
# bar.py (target module)
# unittest.py (module to exclude)
# test_foo.py imports: bar, unittest
# test_qux.py imports: unittest (only)
# When: find_tests_importing_module("bar", exclude_imports=frozenset({"unittest"}))
# Then: test_foo.py is included (bar matches), test_qux.py is excluded

with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = pathlib.Path(tmpdir)
lib_dir = tmpdir / "Lib"
test_dir = lib_dir / "test"
test_dir.mkdir(parents=True)

(lib_dir / "bar.py").write_text("# bar module")
(lib_dir / "unittest.py").write_text("# unittest module")

# test_foo imports both bar and unittest
(test_dir / "test_foo.py").write_text("import bar\nimport unittest\n")
# test_qux imports only unittest
(test_dir / "test_qux.py").write_text("import unittest\n")

get_transitive_imports.cache_clear()
find_tests_importing_module.cache_clear()
result = find_tests_importing_module(
"bar",
lib_prefix=str(lib_dir),
exclude_imports=frozenset({"unittest"})
)

# test_foo.py should be included (imports bar)
self.assertIn(test_dir / "test_foo.py", result)
# test_qux.py should be excluded (only imports unittest)
self.assertNotIn(test_dir / "test_qux.py", result)

def test_exclude_transitive_via_excluded_module(self):
"""Test that transitive dependencies via excluded modules are also excluded."""
# Given:
# bar.py (target)
# baz.py imports bar
# unittest.py imports baz (so unittest transitively depends on bar)
# test_foo.py imports unittest (only)
# When: find_tests_importing_module("bar", exclude_imports=frozenset({"unittest"}))
# Then: test_foo.py is NOT included (unittest is excluded, no other path to bar)

with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = pathlib.Path(tmpdir)
lib_dir = tmpdir / "Lib"
test_dir = lib_dir / "test"
test_dir.mkdir(parents=True)

(lib_dir / "bar.py").write_text("# bar module")
(lib_dir / "baz.py").write_text("import bar\n")
(lib_dir / "unittest.py").write_text("import baz\n") # unittest -> baz -> bar

# test_foo imports only unittest
(test_dir / "test_foo.py").write_text("import unittest\n")

get_transitive_imports.cache_clear()
find_tests_importing_module.cache_clear()
result = find_tests_importing_module(
"bar",
lib_prefix=str(lib_dir),
exclude_imports=frozenset({"unittest"})
)

# test_foo.py should NOT be included
# (even though unittest -> baz -> bar, unittest is excluded)
self.assertNotIn(test_dir / "test_foo.py", result)

def test_exclude_empty_set_same_as_default(self):
"""Test that empty exclude set behaves same as no exclusion."""
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = pathlib.Path(tmpdir)
lib_dir = tmpdir / "Lib"
test_dir = lib_dir / "test"
test_dir.mkdir(parents=True)

(lib_dir / "bar.py").write_text("# bar module")
(test_dir / "test_foo.py").write_text("import bar\n")

get_transitive_imports.cache_clear()
find_tests_importing_module.cache_clear()

result_default = find_tests_importing_module("bar", lib_prefix=str(lib_dir))

find_tests_importing_module.cache_clear()
result_empty = find_tests_importing_module(
"bar",
lib_prefix=str(lib_dir),
exclude_imports=frozenset()
)

self.assertEqual(result_default, result_empty)


if __name__ == "__main__":
unittest.main()