band-aider-pub/tests/band_aider/test_python_source_discovery_module_names.py
2026-10-17 11:57:37 -07:00

283 lines
12 KiB
Python

import json
from pathlib import Path
import pytest
from band_aider.context.class_meta import ClassMeta
from band_aider.judgement.python_ast_dependency_graph_maintainer import (
PythonAstDependencyGraphMaintainer,
)
from band_aider.strategies.python.python_source_discovery import (
PythonSourceDiscovery,
SearchableBeat,
)
from tests.support.memory_path import MemoryPath
from tests.support.memory_repo_exec import MemoryRepoExec
@pytest.fixture
def memory_root() -> MemoryPath:
"""Return a fresh source-discovery tree held entirely in memory."""
return MemoryPath.from_repo(MemoryRepoExec())
def _write(path: Path, content: str = "") -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
def _facts(discovery: PythonSourceDiscovery, path: Path) -> SearchableBeat:
beats = discovery.searchable_beats(str(path), path.read_text(encoding="utf-8"))
return next(
beat
for beat in beats
if isinstance(beat, SearchableBeat) and beat.kind == "static_facts"
)
def test_package_root_absolute_import_builds_graph_edge(memory_root: MemoryPath) -> None:
root = memory_root / "repo"
src_root = root / "band_aider"
_write(src_root / "__init__.py")
_write(src_root / "context" / "__init__.py")
_write(src_root / "context" / "memento.py", "class Memento: pass\n")
class_meta_path = src_root / "context" / "class_meta.py"
_write(class_meta_path, "from band_aider.context.memento import Memento\n")
discovery = PythonSourceDiscovery(src_root, root / "tests", repo_exec=memory_root.repository)
memento_facts = _facts(discovery, src_root / "context" / "memento.py")
class_meta_facts = _facts(discovery, class_meta_path)
assert dict(memento_facts.attributes)["module_name"] == "band_aider.context.memento"
assert any(
item[0] == "band_aider.context.memento"
for item in json.loads(dict(class_meta_facts.attributes)["imported_names"])
)
source_paths = [src_root / "context" / "memento.py", class_meta_path]
source_files = [
ClassMeta(
location=str(path.relative_to(root)),
summary="",
relevance=[],
relevant_symbols={},
)
for path in source_paths
]
maintainer = PythonAstDependencyGraphMaintainer(repo_exec=memory_root.repository)
graph = maintainer.build(
source_files, [], root, src_root, root / "tests"
)
assert graph.source_dependencies["band_aider/context/class_meta.py"] == {
"band_aider/context/memento.py"
}
assert "band_aider/context/class_meta.py" in graph.source_dependents[
"band_aider/context/memento.py"
]
def test_src_layout_and_package_initializer_names(memory_root: MemoryPath) -> None:
root = memory_root / "repo"
src_root = root / "src"
package = src_root / "band_aider"
_write(package / "__init__.py")
_write(package / "context" / "__init__.py")
memento = package / "context" / "memento.py"
_write(memento)
discovery = PythonSourceDiscovery(src_root, root / "tests", repo_exec=memory_root.repository)
assert dict(_facts(discovery, memento).attributes)["module_name"] == "band_aider.context.memento"
assert discovery._module_name(str(package / "context" / "__init__.py")) == "band_aider.context"
def test_relative_imports_keep_existing_module_identity(memory_root: MemoryPath) -> None:
root = memory_root / "repo"
src_root = root / "band_aider"
_write(src_root / "__init__.py")
_write(src_root / "ui" / "__init__.py")
splash = src_root / "ui" / "splash.py"
startup = src_root / "ui" / "startup_wizard.py"
_write(splash, "ASCII_ART = 'x'\n")
_write(startup, "from .splash import ASCII_ART\n")
discovery = PythonSourceDiscovery(src_root, root / "tests", repo_exec=memory_root.repository)
imported = json.loads(dict(_facts(discovery, startup).attributes)["imported_names"])
assert any(item[0] == "band_aider.ui.splash" for item in imported)
def test_unrelated_repeated_root_name_is_rejected(memory_root: MemoryPath) -> None:
root = memory_root / "repo"
src_root = root / "src"
_write(src_root / "__init__.py")
discovery = PythonSourceDiscovery(
src_root,
root / "tests",
repo_exec=memory_root.repository,
repo_root=root,
)
with pytest.raises(ValueError, match="outside configured source and test roots"):
discovery._module_name(str(root / "other" / "src" / "module.py"))
def test_dynamic_resolution_uses_canonical_qualified_type_name(memory_root: MemoryPath) -> None:
root = memory_root / "repo"
src_root = root / "src"
files = {
"package_a/__init__.py": "",
"package_a/result.py": "class Result: pass\n",
"package_b/__init__.py": "",
"package_b/result.py": "class Result: pass\n",
"consumer.py": (
"from package_a.result import Result\n"
"class Consumer:\n"
" def __init__(self, result: Result): pass\n"
"Consumer(Result())\n"
),
}
for location, source in files.items():
_write(src_root / location, source)
source_files = [
ClassMeta(
location=str((src_root / location).relative_to(root)),
summary="",
relevance=[],
relevant_symbols={},
)
for location in files
]
maintainer = PythonAstDependencyGraphMaintainer(repo_exec=memory_root.repository)
graph = maintainer.build(
source_files, [], root, src_root, root / "tests"
)
result_types = {
record.qualified_name: record.type_id
for record in graph.type_definitions.values()
if record.local_name == "Result"
}
assert result_types["package_a.result.Result"] != result_types["package_b.result.Result"]
resolved = maintainer._resolve_type(
"Result",
maintainer.graph.type_definitions,
"src/consumer.py",
)
assert resolved == result_types["package_a.result.Result"]
def test_incremental_src_creation_preserves_import_root(memory_root: MemoryPath) -> None:
root = memory_root / "repo"
src_root = root / "src"
files = {
"package_a/__init__.py": "",
"package_a/dependency.py": "class Dependency: pass\n",
"package_a/consumer.py": "from package_a.dependency import Dependency\n",
}
for location, source in files.items():
_write(src_root / location, source)
source_files = [
ClassMeta(
location=f"src/{location}",
summary="",
relevance=[],
relevant_symbols={},
)
for location in files
]
maintainer = PythonAstDependencyGraphMaintainer(repo_exec=memory_root.repository)
graph = maintainer.build(source_files, [], root, src_root, root / "tests")
assert not (src_root / "__init__.py").exists()
assert maintainer._static_facts_by_location["src/package_a/dependency.py"].module_name == "package_a.dependency"
assert maintainer._static_facts_by_location["src/package_a/consumer.py"].module_name == "package_a.consumer"
assert graph.source_dependencies["src/package_a/consumer.py"] == {"src/package_a/dependency.py"}
new_path = src_root / "package_a/new_area/new_source.py"
_write(new_path, "class NewSource: pass\n")
new_meta = ClassMeta("src/package_a/new_area/new_source.py", "", [], {})
graph = maintainer.upsert(
new_meta,
[*source_files, new_meta],
[],
root,
src_root,
root / "tests",
)
assert not (src_root / "__init__.py").exists()
assert (src_root / "package_a/new_area/__init__.py").exists()
assert maintainer._static_facts_by_location["src/package_a/dependency.py"].module_name == "package_a.dependency"
assert maintainer._static_facts_by_location["src/package_a/consumer.py"].module_name == "package_a.consumer"
assert maintainer._static_facts_by_location["src/package_a/new_area/new_source.py"].module_name == "package_a.new_area.new_source"
assert graph.source_dependencies["src/package_a/consumer.py"] == {"src/package_a/dependency.py"}
assert all("src." not in value for value in graph.source_by_location)
assert all("src." not in value for value in maintainer._static_facts_by_location)
def test_package_root_incremental_creation_keeps_package_prefix(memory_root: MemoryPath) -> None:
root = memory_root / "repo"
src_root = root / "package_a"
_write(src_root / "__init__.py")
_write(src_root / "existing.py", "class Existing: pass\n")
existing = ClassMeta("package_a/existing.py", "", [], {})
maintainer = PythonAstDependencyGraphMaintainer(repo_exec=memory_root.repository)
maintainer.build([existing], [], root, src_root, root / "tests")
new_path = src_root / "new_area/new_source.py"
_write(new_path, "class NewSource: pass\n")
new = ClassMeta("package_a/new_area/new_source.py", "", [], {})
maintainer.upsert(new, [existing, new], [], root, src_root, root / "tests")
assert (src_root / "__init__.py").exists()
assert (src_root / "new_area/__init__.py").exists()
assert maintainer._static_facts_by_location[existing.location].module_name == "package_a.existing"
assert maintainer._static_facts_by_location[new.location].module_name == "package_a.new_area.new_source"
def test_test_mapping_uses_discovered_module_facts_for_both_layouts(memory_root: MemoryPath) -> None:
for layout in ("package_root", "src"):
root = memory_root / layout
src_root = root / "package_a" if layout == "package_root" else root / "src"
package_root = src_root if layout == "package_root" else src_root / "package_a"
_write(package_root / "__init__.py")
dependency_path = package_root / "dependency.py"
_write(dependency_path, "class Dependency: pass\n")
test_path = root / "tests/test_dependency.py"
_write(test_path, "from package_a.dependency import Dependency\n")
source_location = (
"package_a/dependency.py"
if layout == "package_root"
else "src/package_a/dependency.py"
)
source = ClassMeta(source_location, "", [], {})
test = ClassMeta("tests/test_dependency.py", "", [], {})
maintainer = PythonAstDependencyGraphMaintainer(repo_exec=memory_root.repository)
graph = maintainer.build(
[source],
[test],
root,
src_root,
root / "tests",
)
assert source_location in graph.tests_for_source
assert test.location in graph.tests_for_source[source_location]
def test_incremental_static_edges_follow_import_changes_and_new_dependencies(memory_root: MemoryPath) -> None:
root = memory_root / "repo"
src_root = root / "src"
_write(src_root / "package_a/__init__.py")
_write(src_root / "package_a/old_dependency.py", "class Dependency: pass\n")
_write(src_root / "package_a/new_dependency.py", "class Dependency: pass\n")
importer_path = src_root / "package_a/consumer.py"
_write(importer_path, "from package_a.old_dependency import Dependency\n")
old = ClassMeta("src/package_a/old_dependency.py", "", [], {})
new = ClassMeta("src/package_a/new_dependency.py", "", [], {})
importer = ClassMeta("src/package_a/consumer.py", "", [], {})
maintainer = PythonAstDependencyGraphMaintainer(repo_exec=memory_root.repository)
graph = maintainer.build([old, new, importer], [], root, src_root, root / "tests")
assert graph.source_dependencies[importer.location] == {old.location}
_write(importer_path, "from package_a.new_dependency import Dependency\n")
graph = maintainer.upsert(importer, [old, new, importer], [], root, src_root, root / "tests")
assert graph.source_dependencies[importer.location] == {new.location}
assert importer.location not in graph.source_dependents.get(old.location, set())
assert importer.location in graph.source_dependents[new.location]
_write(src_root / "package_a/new_dependency.py", "class Dependency: pass\nvalue = 1\n")
graph = maintainer.upsert(new, [old, new, importer], [], root, src_root, root / "tests")
assert graph.source_dependencies[importer.location] == {new.location}