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
Exclude non-test files from deps subcommand impact output
  • Loading branch information
moreal authored and youknowone committed Jan 22, 2026
commit 4c116cd0a403ae4eb3a37e21f78af3dfc8dc6dc1
79 changes: 61 additions & 18 deletions scripts/update_lib/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,37 @@ def get_transitive_imports(
return frozenset(visited)


def _build_test_import_graph(test_dir: pathlib.Path) -> dict[str, set[str]]:
"""Build import graph for files within test directory.

Args:
test_dir: Path to Lib/test/ directory

Returns:
Dict mapping filename (stem) -> set of test modules it imports
"""
import_graph: dict[str, set[str]] = {}

for py_file in test_dir.glob("*.py"):
content = safe_read_text(py_file)
if content is None:
continue

imports = set()
# Parse "from test import X" style imports
imports.update(parse_test_imports(content))
# Also check direct imports of test modules
all_imports = parse_lib_imports(content)
# Filter to only test modules that exist in test_dir
for imp in all_imports:
if (test_dir / f"{imp}.py").exists():
imports.add(imp)

import_graph[py_file.stem] = imports

return import_graph


@functools.cache
def find_tests_importing_module(
module_name: str,
Expand All @@ -609,46 +640,58 @@ def find_tests_importing_module(
) -> frozenset[pathlib.Path]:
"""Find all test files that import the given module (directly or transitively).

Only returns test_*.py files. Support files (like pickletester.py, string_tests.py)
are used for transitive dependency calculation but not included in the result.

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
Frozenset of test_*.py file paths that depend on this module
"""
lib_dir = pathlib.Path(lib_prefix)
test_dir = lib_dir / "test"

if not test_dir.exists():
return frozenset()

# Build set of modules to search for
# Build set of modules to search for (Lib/ modules)
target_modules = {module_name}
if include_transitive:
# Add all modules that transitively depend on module_name
target_modules.update(get_transitive_imports(module_name, lib_prefix))

# Excluded test file for this module (test_<module>.py)
excluded_test = f"test_{module_name}.py"

# Scan test directory for files that import any of the target modules
result: set[pathlib.Path] = set()

for test_file in test_dir.glob("*.py"):
if test_file.name == excluded_test:
continue
# Build test directory import graph for transitive analysis within test/
test_import_graph = _build_test_import_graph(test_dir)

content = safe_read_text(test_file)
# First pass: find all files (by stem) that directly import target modules
directly_importing: set[str] = set()
for py_file in test_dir.glob("*.py"):
content = safe_read_text(py_file)
if content is None:
continue

imports = parse_lib_imports(content)
# Remove excluded modules from imports
imports = imports - exclude_imports
# Check if any target module is imported
imports = parse_lib_imports(content) - exclude_imports
if imports & target_modules:
result.add(test_file)
directly_importing.add(py_file.stem)

# Second pass: find files that transitively import via support files within test/
# BFS to find all files that import any file in all_importing
all_importing = set(directly_importing)
queue = list(directly_importing)
while queue:
current = queue.pop(0)
for file_stem, imports in test_import_graph.items():
if current in imports and file_stem not in all_importing:
all_importing.add(file_stem)
queue.append(file_stem)

# Filter to only test_*.py files and build result paths
result: set[pathlib.Path] = set()
for file_stem in all_importing:
if file_stem.startswith("test_"):
result.add(test_dir / f"{file_stem}.py")

return frozenset(result)
100 changes: 96 additions & 4 deletions scripts/update_lib/tests/test_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,8 +495,8 @@ def test_direct_import(self):
result = find_tests_importing_module("bar", lib_prefix=str(lib_dir))
self.assertIn(test_dir / "test_foo.py", result)

def test_excludes_test_module_itself(self):
"""Test that test_<module>.py is excluded from results."""
def test_includes_test_module_itself(self):
"""Test that test_<module>.py IS included in results."""
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = pathlib.Path(tmpdir)
lib_dir = tmpdir / "Lib"
Expand All @@ -509,8 +509,8 @@ def test_excludes_test_module_itself(self):
get_transitive_imports.cache_clear()
find_tests_importing_module.cache_clear()
result = find_tests_importing_module("bar", lib_prefix=str(lib_dir))
# test_bar.py should NOT be in results (it's the primary test)
self.assertNotIn(test_dir / "test_bar.py", result)
# test_bar.py IS now included (module's own test is part of impact)
self.assertIn(test_dir / "test_bar.py", result)

def test_transitive_import(self):
"""Test finding tests with transitive (indirect) imports."""
Expand Down Expand Up @@ -653,5 +653,97 @@ def test_exclude_empty_set_same_as_default(self):
self.assertEqual(result_default, result_empty)


class TestFindTestsOnlyTestFiles(unittest.TestCase):
"""Tests for filtering to only test_*.py files in output."""

def test_support_file_not_in_output(self):
"""Support files should not appear in output even if they import target."""
# Given:
# bar.py (target module in Lib/)
# helper.py (support file in test/, imports bar)
# test_foo.py (test file, imports bar)
# When: find_tests_importing_module("bar")
# Then: test_foo.py is included, helper.py is NOT included

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")
# helper.py imports bar directly but doesn't start with test_
(test_dir / "helper.py").write_text("import bar\n")
# test_foo.py also imports bar
(test_dir / "test_foo.py").write_text("import bar\n")

get_transitive_imports.cache_clear()
find_tests_importing_module.cache_clear()
result = find_tests_importing_module("bar", lib_prefix=str(lib_dir))

# Only test_foo.py should be in results
self.assertIn(test_dir / "test_foo.py", result)
# helper.py should be excluded
self.assertNotIn(test_dir / "helper.py", result)

def test_transitive_via_support_file(self):
"""Test file importing support file that imports target should be included."""
# Given:
# bar.py (target module in Lib/)
# helper.py (support file in test/, imports bar)
# test_foo.py (test file, imports helper - NOT bar directly)
# When: find_tests_importing_module("bar")
# Then: test_foo.py IS included (via helper.py), helper.py is NOT

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")
# helper.py imports bar
(test_dir / "helper.py").write_text("import bar\n")
# test_foo.py imports only helper (not bar directly)
(test_dir / "test_foo.py").write_text("from test import helper\n")

get_transitive_imports.cache_clear()
find_tests_importing_module.cache_clear()
result = find_tests_importing_module("bar", lib_prefix=str(lib_dir))

# test_foo.py depends on bar via helper, so it should be included
self.assertIn(test_dir / "test_foo.py", result)
# helper.py should be excluded from output
self.assertNotIn(test_dir / "helper.py", result)

def test_chain_through_multiple_support_files(self):
"""Test transitive chain through multiple support files."""
# Given:
# bar.py (target)
# helper_a.py imports bar
# helper_b.py imports helper_a
# test_foo.py imports helper_b
# Then: test_foo.py IS included, helper_a/b are NOT

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 / "helper_a.py").write_text("import bar\n")
(test_dir / "helper_b.py").write_text("from test import helper_a\n")
(test_dir / "test_foo.py").write_text("from test import helper_b\n")

get_transitive_imports.cache_clear()
find_tests_importing_module.cache_clear()
result = find_tests_importing_module("bar", lib_prefix=str(lib_dir))

self.assertIn(test_dir / "test_foo.py", result)
self.assertNotIn(test_dir / "helper_a.py", result)
self.assertNotIn(test_dir / "helper_b.py", result)


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