461 lines
14 KiB
Python
461 lines
14 KiB
Python
# SPDX-FileCopyrightText: 2026 Art Sea LLC
|
|
#
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
"""Regression coverage for source-driven Python dependency reference cards."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from io import StringIO
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
from typing import Iterable, Sequence, TextIO
|
|
|
|
import pytest
|
|
|
|
from band_aider.context.class_meta import ClassMeta, ContentType
|
|
from band_aider.judgement.code_parser import CodeParser
|
|
from band_aider.judgement.file_manager import FileManager
|
|
from band_aider.judgement.prompt_formatter import MarkdownPromptFormatterBuilder
|
|
from band_aider.judgement.repo_exec import RepoExec
|
|
from band_aider.strategies.language_strategy_set import LanguageStrategySet
|
|
from band_aider.strategies.oop_signature_extract import (
|
|
OOpSignature,
|
|
OOpSignatureExtract,
|
|
OOpSignatureRender,
|
|
)
|
|
from band_aider.strategies.python.python_language_strategy_set import (
|
|
build_python_language_strategy_set,
|
|
)
|
|
from band_aider.strategies.python.python_oop_signature_extract import (
|
|
PythonOOpSignatureExtract,
|
|
)
|
|
from band_aider.strategies.python.python_oop_signature_render import (
|
|
PythonOOpSignatureRender,
|
|
)
|
|
from tests.support.completion_clients import StaticOpenAICompatibleClient
|
|
from tests.support.memory_path import MemoryPath
|
|
from tests.support.memory_repo_exec import MemoryRepoExec
|
|
|
|
|
|
class _CardParser(CodeParser):
|
|
"""Supply nominal FileManager metadata without reading host paths."""
|
|
|
|
def parse_file(
|
|
self,
|
|
file_name: str,
|
|
prompt: str,
|
|
content_type: ContentType = ContentType.Source,
|
|
) -> ClassMeta:
|
|
"""Return one empty metadata record for a lexical in-memory path."""
|
|
|
|
del prompt
|
|
return ClassMeta(file_name, "", [], {}, content_type=content_type)
|
|
|
|
def parse_stream(
|
|
self,
|
|
file_name: str,
|
|
stream: TextIO,
|
|
prompt: str,
|
|
content_type: ContentType = ContentType.Source,
|
|
) -> ClassMeta:
|
|
"""Return metadata without consuming a host-backed stream."""
|
|
|
|
del stream
|
|
return self.parse_file(file_name, prompt, content_type)
|
|
|
|
def parse_files(
|
|
self,
|
|
file_names: list[str],
|
|
prompt: str,
|
|
content_type: ContentType = ContentType.Source,
|
|
) -> list[ClassMeta]:
|
|
"""Return metadata in the supplied lexical path order."""
|
|
|
|
return [self.parse_file(file_name, prompt, content_type) for file_name in file_names]
|
|
|
|
|
|
class _CardFileManager(FileManager):
|
|
"""Bind language strategy construction to one in-memory reference-card project."""
|
|
|
|
def __init__(self, root: MemoryPath, repo_exec: RepoExec | None = None) -> None:
|
|
"""Construct a nominal manager whose repository boundary never reaches the host."""
|
|
|
|
super().__init__(
|
|
parser=_CardParser(),
|
|
client=StaticOpenAICompatibleClient('{"cdc": ""}'),
|
|
repo_root=root,
|
|
src_root=root / "src",
|
|
test_root=root / "tests",
|
|
actor="test",
|
|
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
|
|
repo_exec=root.repository if repo_exec is None else repo_exec,
|
|
prompt="reference cards",
|
|
)
|
|
|
|
def resync(self) -> None:
|
|
"""Keep card construction independent from source discovery metadata IO."""
|
|
|
|
self.src_classes = []
|
|
self.test_classes = []
|
|
self.src_index = {}
|
|
self.test_index = {}
|
|
self.src_dirty.clear()
|
|
self.test_dirty.clear()
|
|
|
|
|
|
class _ExternalSourceRepoExec(MemoryRepoExec):
|
|
"""Return stationary target-environment declaration source through RepoExec."""
|
|
|
|
def run_cli(
|
|
self,
|
|
command: Sequence[str],
|
|
*,
|
|
cwd: Path | None = None,
|
|
env: dict[str, str] | None = None,
|
|
capture_output: bool = True,
|
|
text: bool = True,
|
|
check: bool = False,
|
|
timeout: int | None = None,
|
|
input: bytes | str | None = None,
|
|
unset_env: Iterable[str] = (),
|
|
) -> subprocess.CompletedProcess:
|
|
"""Return one static external stub without starting a process or reading disk."""
|
|
|
|
del cwd, env, capture_output, text, check, timeout, input, unset_env
|
|
self.commands.append(list(command))
|
|
return subprocess.CompletedProcess(
|
|
list(command),
|
|
0,
|
|
json.dumps(
|
|
[
|
|
{
|
|
"path": "/target/site-packages/external_widget.pyi",
|
|
"source": (
|
|
"class Widget:\n"
|
|
" \"\"\"Third-party widget.\"\"\"\n"
|
|
" def run(self) -> bool: ...\n"
|
|
),
|
|
}
|
|
]
|
|
),
|
|
"",
|
|
)
|
|
|
|
def read_text(self, path: Path, *, encoding: str = "utf-8") -> str:
|
|
"""Reject accidental host-style reads of target-environment declaration paths."""
|
|
|
|
if str(path).startswith("/target/site-packages"):
|
|
raise AssertionError("external paths must only be read in target environment")
|
|
return super().read_text(path, encoding=encoding)
|
|
|
|
|
|
@pytest.fixture
|
|
def memory_root() -> MemoryPath:
|
|
"""Return one fresh lexical root backed by an in-memory repository."""
|
|
|
|
return MemoryPath.from_repo(MemoryRepoExec())
|
|
|
|
|
|
def _strategies(
|
|
root: MemoryPath,
|
|
repo_exec: RepoExec | None = None,
|
|
) -> LanguageStrategySet:
|
|
"""Build reference-card strategies through the selected Python language set."""
|
|
|
|
return build_python_language_strategy_set(_CardFileManager(root, repo_exec))
|
|
|
|
|
|
def _extractor(
|
|
root: MemoryPath,
|
|
repo_exec: RepoExec | None = None,
|
|
) -> OOpSignatureExtract:
|
|
"""Resolve the generic extraction contract from the selected Python strategies."""
|
|
|
|
extractor = _strategies(root, repo_exec)[OOpSignatureExtract]
|
|
assert isinstance(extractor, PythonOOpSignatureExtract)
|
|
return extractor
|
|
|
|
|
|
def _write(root: MemoryPath, location: str, text: str) -> Path:
|
|
"""Store one source fixture through the registered in-memory repository boundary."""
|
|
|
|
path = root / location
|
|
root.repository.write_text(path, text)
|
|
return path
|
|
|
|
|
|
def _render(root: MemoryPath, card: OOpSignature) -> str:
|
|
"""Render one card through the selected Python declaration rendering strategy."""
|
|
|
|
output = StringIO()
|
|
renderer = _strategies(root)[OOpSignatureRender]
|
|
assert isinstance(renderer, PythonOOpSignatureRender)
|
|
renderer.render(card, output)
|
|
return output.getvalue()
|
|
|
|
|
|
def test_python_extract_formally_implements_language_neutral_contract() -> None:
|
|
"""The concrete extractor explicitly inherits the project protocol."""
|
|
|
|
assert OOpSignatureExtract in PythonOOpSignatureExtract.__bases__
|
|
assert OOpSignatureRender in PythonOOpSignatureRender.__bases__
|
|
|
|
|
|
def test_extract_references_includes_public_declaration_surface_and_pydocs(
|
|
memory_root: MemoryPath,
|
|
) -> None:
|
|
"""Public APIs are structured while private names and executable bodies are absent."""
|
|
|
|
_write(
|
|
memory_root,
|
|
"src/pkg/surface.py",
|
|
'''from pathlib import Path
|
|
from typing import ClassVar
|
|
|
|
class Client:
|
|
pass
|
|
|
|
class Surface:
|
|
"""Public surface documentation. More details are deliberately retained in DTOs."""
|
|
|
|
KIND: ClassVar[str] = "surface"
|
|
version = 1
|
|
label: str
|
|
_private_class = "hidden"
|
|
|
|
def __init__(self, root: Path) -> None:
|
|
self.root = root
|
|
self.client = Client()
|
|
self._token = "hidden"
|
|
body_secret = "constructor implementation"
|
|
|
|
@property
|
|
def title(self) -> str:
|
|
"""Return the display title."""
|
|
return "body title secret"
|
|
|
|
def run(self, count: int = 1) -> bool:
|
|
"""Run the surface."""
|
|
body_secret = "method implementation"
|
|
return True
|
|
|
|
def _hidden(self) -> str:
|
|
return "private implementation"
|
|
''',
|
|
)
|
|
consumer = "from pkg.surface import Surface\n\ndef use(value: Surface) -> None:\n pass\n"
|
|
|
|
[card] = _extractor(memory_root).extract_references({"src/consumer.py": consumer})
|
|
|
|
assert (card.module, card.name) == ("pkg.surface", "Surface")
|
|
rendered = _render(memory_root, card)
|
|
assert "class Surface" in rendered
|
|
assert "# Public surface documentation." in rendered
|
|
assert "constructor: __init__(self, root: Path) -> None" in rendered
|
|
assert "KIND: ClassVar[str]" in rendered
|
|
assert "version: int" in rendered
|
|
assert "label: str" in rendered
|
|
assert "root: Path" in rendered
|
|
assert "client: Client" in rendered
|
|
assert "@property title: str" in rendered
|
|
assert "# Return the display title." in rendered
|
|
assert "run(self, count: int = 1) -> bool" in rendered
|
|
assert "# Run the surface." in rendered
|
|
assert "_hidden" not in rendered
|
|
assert "_private_class" not in rendered
|
|
assert "_token" not in rendered
|
|
assert "constructor implementation" not in rendered
|
|
assert "method implementation" not in rendered
|
|
assert "body title secret" not in rendered
|
|
assert "private implementation" not in rendered
|
|
|
|
|
|
def test_extract_source_uses_the_same_compact_public_card_surface(
|
|
memory_root: MemoryPath,
|
|
) -> None:
|
|
"""Current source cards retain declarations and pydocs without executable bodies."""
|
|
|
|
source = '''class Surface:
|
|
"""Current surface documentation."""
|
|
|
|
enabled: bool
|
|
|
|
@property
|
|
def title(self) -> str:
|
|
"""Return the current title."""
|
|
return "hidden body"
|
|
|
|
def run(self, count: int = 1) -> bool:
|
|
"""Run the current surface."""
|
|
return True
|
|
|
|
def build(name: str) -> Surface:
|
|
"""Build a current surface."""
|
|
return Surface()
|
|
|
|
_secret = "hidden"
|
|
'''
|
|
|
|
cards = _extractor(memory_root).extract_source("src/pkg/current.py", source)
|
|
|
|
assert [(card.module, card.name) for card in cards] == [
|
|
("pkg.current", "Surface"),
|
|
("pkg.current", "build"),
|
|
]
|
|
surface = _render(memory_root, cards[0])
|
|
assert "# Current surface documentation." in surface
|
|
assert "enabled: bool" in surface
|
|
assert "@property title: str" in surface
|
|
assert "# Return the current title." in surface
|
|
assert "run(self, count: int = 1) -> bool" in surface
|
|
assert "hidden body" not in surface
|
|
|
|
|
|
def test_extract_references_shows_both_internal_mismatch_dependencies(
|
|
memory_root: MemoryPath,
|
|
) -> None:
|
|
"""Two imported internal types remain available without diagnostic-string scraping."""
|
|
|
|
_write(
|
|
memory_root,
|
|
"tests/support/file_manager.py",
|
|
'''class _FileManager:
|
|
"""Lightweight test file manager."""
|
|
|
|
def __init__(self, root: str) -> None:
|
|
self.root = root
|
|
|
|
def source_path(self, location: str) -> str:
|
|
return location
|
|
''',
|
|
)
|
|
_write(
|
|
memory_root,
|
|
"src/pkg/file_manager.py",
|
|
'''class FileManager:
|
|
def source_path(self, location: str) -> str:
|
|
return location
|
|
''',
|
|
)
|
|
source = (
|
|
"from tests.support.file_manager import _FileManager\n"
|
|
"from pkg.file_manager import FileManager\n"
|
|
"\n"
|
|
"def consume(manager: FileManager) -> None:\n"
|
|
" pass\n"
|
|
)
|
|
|
|
cards = _extractor(memory_root).extract_references({"src/pkg/consumer.py": source})
|
|
|
|
assert [(card.module, card.name) for card in cards] == [
|
|
("tests.support.file_manager", "_FileManager"),
|
|
("pkg.file_manager", "FileManager"),
|
|
]
|
|
assert "constructor: __init__(self, root: str) -> None" in _render(memory_root, cards[0])
|
|
assert "source_path(self, location: str) -> str" in _render(memory_root, cards[1])
|
|
|
|
|
|
def test_extract_references_prefers_repository_stub_declarations(
|
|
memory_root: MemoryPath,
|
|
) -> None:
|
|
"""A colocated pyi surface wins over implementation-only members."""
|
|
|
|
_write(
|
|
memory_root,
|
|
"src/pkg/service.py",
|
|
"class Service:\n def runtime_only(self) -> int:\n return 1\n",
|
|
)
|
|
_write(
|
|
memory_root,
|
|
"src/pkg/service.pyi",
|
|
"class Service:\n def declared(self, value: str) -> bool: ...\n",
|
|
)
|
|
|
|
[card] = _extractor(memory_root).extract_references(
|
|
{"src/consumer.py": "from pkg.service import Service\n"}
|
|
)
|
|
|
|
rendered = _render(memory_root, card)
|
|
assert "declared(self, value: str) -> bool" in rendered
|
|
assert "runtime_only" not in rendered
|
|
|
|
|
|
def test_extract_references_reads_external_declarations_in_target_environment(
|
|
memory_root: MemoryPath,
|
|
) -> None:
|
|
"""External package source is provided by a stationary target RepoExec response."""
|
|
|
|
repo_exec = _ExternalSourceRepoExec(memory_root)
|
|
extractor = _extractor(memory_root, repo_exec)
|
|
|
|
[card] = extractor.extract_references(
|
|
{"src/consumer.py": "from external_widget import Widget\n"}
|
|
)
|
|
|
|
assert (card.module, card.name) == ("external_widget", "Widget")
|
|
assert "# Third-party widget." in _render(memory_root, card)
|
|
assert "run(self) -> bool" in _render(memory_root, card)
|
|
|
|
|
|
def test_extract_references_excludes_visible_files_and_refreshes_dependency_source(
|
|
memory_root: MemoryPath,
|
|
) -> None:
|
|
"""Visible source is not duplicated and a hidden dependency is re-read every time."""
|
|
|
|
dependency = _write(
|
|
memory_root,
|
|
"src/pkg/hidden.py",
|
|
"class Hidden:\n def first(self) -> None: ...\n",
|
|
)
|
|
extractor = _extractor(memory_root)
|
|
source = "from pkg.hidden import Hidden\n"
|
|
|
|
assert extractor.extract_references(
|
|
{
|
|
"src/consumer.py": source,
|
|
"src/pkg/hidden.py": memory_root.repository.read_text(dependency),
|
|
}
|
|
) == ()
|
|
|
|
[first] = extractor.extract_references({"src/consumer.py": source})
|
|
assert "first(self) -> None" in _render(memory_root, first)
|
|
memory_root.repository.write_text(
|
|
dependency,
|
|
"class Hidden:\n def second(self) -> None: ...\n",
|
|
)
|
|
[second] = extractor.extract_references({"src/consumer.py": source})
|
|
assert "second(self) -> None" in _render(memory_root, second)
|
|
assert "first(self) -> None" not in _render(memory_root, second)
|
|
|
|
|
|
def test_module_import_exposes_public_top_level_declarations(
|
|
memory_root: MemoryPath,
|
|
) -> None:
|
|
"""A module import provides discoverable public classes, functions, and constants."""
|
|
|
|
_write(
|
|
memory_root,
|
|
"src/pkg/api.py",
|
|
'''class Client:
|
|
pass
|
|
|
|
def build(name: str) -> Client:
|
|
return Client()
|
|
|
|
DEFAULT_NAME = "client"
|
|
_private = "hidden"
|
|
''',
|
|
)
|
|
|
|
cards = _extractor(memory_root).extract_references(
|
|
{"src/consumer.py": "import pkg.api\n"}
|
|
)
|
|
|
|
assert [(card.module, card.name) for card in cards] == [
|
|
("pkg.api", "Client"),
|
|
("pkg.api", "build"),
|
|
("pkg.api", "DEFAULT_NAME"),
|
|
]
|
|
assert "def build(name: str) -> Client" in _render(memory_root, cards[1])
|