band-aider-pub/tests/aspects/test_aspect.py
2026-10-17 15:25:16 -07:00

304 lines
9.5 KiB
Python

# SPDX-FileCopyrightText: 2026 Art Sea LLC
#
# SPDX-License-Identifier: AGPL-3.0-only
"""Behavioral tests for per-invocation argument aspects."""
from __future__ import annotations
import inspect
from typing import Callable, Generic, Protocol, TypeVar, cast
import pytest
from band_aider.aspects import Aspect, Caller, aspect
from band_aider.aspects.aspect import AspectAmbiguityError, AspectResolutionError
T = TypeVar("T")
class Source:
"""Source supplies itself through a direct invocation frame."""
def invoke(self, target: Target) -> tuple[Source, str]:
"""Invoke one target without supplying its caller aspect."""
injected_execute = cast(Callable[[], tuple[Source, str]], target.execute)
return injected_execute()
class Target:
"""Target resolves a typed source aspect."""
@aspect
def execute(self, caller: Caller[Source]) -> tuple[Source, str]:
"""Return the typed value and the actual direct caller frame name."""
frame = caller.frame
assert frame is not None
return caller.value, frame.f_code.co_name
def test_caller_injects_the_typed_direct_caller_without_exposing_wrapper_frame() -> None:
"""An omitted Caller resolves the typed value from the actual invoker frame."""
source = Source()
value, frame_name = source.invoke(Target())
assert value is source
assert frame_name == "invoke"
def test_caller_does_not_depend_on_the_receiver_name() -> None:
"""Resolution is type-based even when a caller receiver is not named self."""
class AlternateSource:
"""Use a nonstandard receiver name."""
def invoke(this, target: AlternateTarget) -> AlternateSource: # pyright: ignore[reportSelfClsParameterName]
"""Invoke one target while retaining the alternate receiver value."""
injected_execute = cast(
Callable[[], AlternateSource],
target.execute,
)
return injected_execute()
class AlternateTarget:
"""Resolve AlternateSource from the invocation frame."""
@aspect
def execute(self, caller: Caller[AlternateSource]) -> AlternateSource:
"""Return the resolved alternate source."""
return caller.value
source = AlternateSource()
assert source.invoke(AlternateTarget()) is source
def test_explicit_aspect_argument_is_preserved() -> None:
"""An explicit aspect-bearing argument remains untouched by injection."""
class OverrideTarget:
"""Expose the supplied argument directly."""
@aspect
def execute(self, caller: Caller[Source]) -> object:
"""Return the explicit caller value unchanged."""
return caller
def provided_caller(source: Source) -> Caller[Source]:
"""Build one real explicit Caller value from this test's active frame."""
frame = inspect.currentframe()
assert frame is not None
return Caller(frame, (source,), {}, Source)
provided = provided_caller(Source())
assert OverrideTarget().execute(provided) is provided
provided.release()
def test_missing_caller_value_fails_with_decorated_parameter_context() -> None:
"""An absent compatible caller value is an explicit resolution failure."""
class MissingTarget:
"""Require a Source unavailable in the calling frame."""
@aspect
def execute(self, caller: Caller[Source]) -> Source:
"""Return the caller only if resolution succeeds."""
return caller.value
def invoke_without_source(target: MissingTarget) -> Source:
"""Invoke from a frame without a Source value."""
injected_execute = cast(Callable[[], Source], target.execute)
return injected_execute()
with pytest.raises(AspectResolutionError, match=r"MissingTarget\.execute\.caller"):
invoke_without_source(MissingTarget())
def test_ambiguous_caller_values_fail_instead_of_selecting_one() -> None:
"""Several compatible values in one caller frame are an explicit ambiguity."""
class AmbiguousTarget:
"""Require one Source from an intentionally ambiguous invocation."""
@aspect
def execute(self, caller: Caller[Source]) -> Source:
"""Return the caller only if resolution succeeds."""
return caller.value
def invoke_with_two_sources(target: AmbiguousTarget) -> Source:
"""Expose two distinct Source values in the same direct caller frame."""
first = Source()
second = Source()
injected_execute = cast(Callable[[], Source], target.execute)
return injected_execute()
with pytest.raises(AspectAmbiguityError, match=r"AmbiguousTarget\.execute\.caller"):
invoke_with_two_sources(AmbiguousTarget())
def test_caller_rejects_a_protocol_shaped_value_without_formal_implementation() -> None:
"""A Caller protocol target requires explicit inheritance rather than matching attributes."""
class Formatter(Protocol):
"""Formatter requires a declared rendering contract."""
depth: int
def __call__(self, body: str) -> str:
"""Render one body."""
...
class Lookalike:
"""Lookalike has the attributes but does not implement Formatter formally."""
depth = 4
def __call__(self, body: str) -> str:
"""Return the received body."""
return body
class ProtocolTarget:
"""Resolve a formally implemented formatter from its direct caller."""
@aspect
def execute(self, caller: Caller[Formatter]) -> Formatter:
"""Return the resolved formatter."""
return caller.value
def invoke(target: ProtocolTarget, candidate: Lookalike) -> Formatter:
"""Expose only the non-formal formatter candidate to the decorated call."""
injected_execute = cast(Callable[[], Formatter], target.execute)
return injected_execute()
with pytest.raises(AspectResolutionError, match=r"ProtocolTarget\.execute\.caller"):
invoke(ProtocolTarget(), Lookalike())
def test_multiple_aspects_resolve_independently_from_one_caller_frame() -> None:
"""Several omitted aspect parameters resolve their distinct target types."""
class Context:
"""Context is a second caller-visible aspect target."""
class MultiTarget:
"""Require both Source and Context aspects."""
@aspect
def execute(
self,
source: Caller[Source],
context: Caller[Context],
) -> tuple[Source, Context]:
"""Return both independently resolved values."""
return source.value, context.value
def invoke(target: MultiTarget, source: Source, context: Context) -> tuple[Source, Context]:
"""Expose exactly one value for each requested aspect target."""
injected_execute = cast(
Callable[[], tuple[Source, Context]],
target.execute,
)
return injected_execute()
source = Source()
context = Context()
assert invoke(MultiTarget(), source, context) == (source, context)
def test_aspect_preserves_ordinary_binding_and_signature() -> None:
"""Aspect injection augments normal positional, keyword, and variadic binding."""
class OrdinaryTarget:
"""Combine ordinary arguments with one injectable caller aspect."""
@aspect
def execute(
self,
value: int,
default: int = 7,
*items: int,
caller: Caller[Source],
label: str = "default",
**extras: int,
) -> tuple[Source, int, int, tuple[int, ...], str, dict[str, int]]:
"""Return ordinary bindings alongside the resolved source."""
return caller.value, value, default, items, label, extras
def invoke(source: Source, target: OrdinaryTarget) -> tuple[Source, int, int, tuple[int, ...], str, dict[str, int]]:
"""Call the decorated method with ordinary argument forms."""
injected_execute = cast(
Callable[..., tuple[Source, int, int, tuple[int, ...], str, dict[str, int]]],
target.execute,
)
return injected_execute(1, 2, 3, 4, label="named", extra=5)
source = Source()
target = OrdinaryTarget()
assert invoke(source, target) == (source, 1, 2, (3, 4), "named", {"extra": 5})
assert "Caller[Source]" in str(
inspect.signature(target.execute).parameters["caller"].annotation
)
def test_aspect_leaves_functions_without_aspect_parameters_ordinary() -> None:
"""Decorating a normal function without aspects preserves ordinary calls."""
class PlainTarget:
"""Expose a normal wrapped method."""
@aspect
def execute(self, value: int) -> int:
"""Return the normal argument without aspect resolution."""
return value
assert PlainTarget().execute(3) == 3
def test_aspect_detection_is_generic_over_aspect_subtypes() -> None:
"""The decorator discovers Aspect subclasses without knowing Caller."""
class Fixed(Aspect[T], Generic[T]):
"""Resolve a fixed value for generic detection coverage."""
def create(self) -> T:
"""Return one fixed value independent of caller lookup."""
return cast(T, 42)
class FixedTarget:
"""Receive one non-Caller aspect value."""
@aspect
def execute(self, fixed: Fixed[int]) -> int:
"""Return the custom aspect value."""
return fixed.value
injected_execute = cast(Callable[[], int], FixedTarget().execute)
assert injected_execute() == 42