Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 11 additions & 1 deletion alternative.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ class MultipleDefaults(AlternativeError):
"""Cannot set the default implementation more than once."""


class CrossAlternativesImplementation(AlternativeError):
"""Cannot add an Implementation object that belongs to a different Alternatives set."""


def get_caller_path() -> str | None:
"""
Return 'module.QualName (file.py:line)' pointing to the line
Expand Down Expand Up @@ -147,7 +151,7 @@ def add(
raise AddTooLate(msg)

if isinstance(implementation, _UNDEFINED):
# FIXME: handle when implementation is for a different set of alternatives

def wrapper(
implementation: ImplementationSig[P, R],
) -> Implementation[P, R]:
Expand All @@ -158,6 +162,12 @@ def wrapper(
label = maybe_get_caller_path()
if not isinstance(implementation, Implementation):
imp = Implementation(self, implementation, label=label)
elif implementation.alternatives is not self:
raise CrossAlternativesImplementation(
f"Cannot add {implementation!r} to {self.reference!r}; "
"it belongs to a different Alternatives set. "
"Pass implementation.implementation to clone explicitly."
)
else:
imp = Implementation(self, implementation.implementation, label=label)

Expand Down
31 changes: 29 additions & 2 deletions test_alternative.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,12 +243,39 @@ def f2():

# alt1 comes from a different set of alternatives of f2
assert isinstance(alt1, alternative.Implementation)
assert f2.add(alt1).alternatives is f2
with pytest.raises(alternative.CrossAlternativesImplementation):
f2.add(alt1)

# when duplicating an implementation, a new Implementation object is returned
# adding an implementation to its own alternatives clones the wrapper
assert f1.add(alt1) is not alt1


def test_cross_owner_add_error():
"""Adding a cross-owner implementation raises a dedicated explicit error."""

@alternative.reference
def source():
return 1

@source.add
def source_alt():
return 2

@alternative.reference
def target():
return 3

expected = (
r"^Cannot add "
r"Implementation\(test_cross_owner_add_error\.<locals>\.source_alt(?:, label='[^']+')?\) "
r"to Implementation\(test_cross_owner_add_error\.<locals>\.target(?:, label='[^']+')?\); "
r"it belongs to a different Alternatives set\. "
r"Pass implementation\.implementation to clone explicitly\.$"
)
with pytest.raises(alternative.CrossAlternativesImplementation, match=expected):
target.add(source_alt)


def test_implementation_label_populated_in_debug(monkeypatch):
"""Implementation labels include caller metadata in debug mode."""
monkeypatch.setattr(alternative, "DEBUG", True)
Expand Down
Loading