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
5 changes: 5 additions & 0 deletions docs/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## 0.3.0b1 (unreleased)

Constructor signature errors now identify an unexpected keyword in the primary
`SignatureError` message and `slot`, including through `Definition(...).build()`.
The stable `"argument binding failed"` reason and original exception cause are
retained; argument values are not rendered. See [Signatures](signatures.md#errors).

Maintained dense CPU estimator adapters now provide XGBoost classification and
scalar regression (`dryml.models.xgb`) and statsmodels OLS/binary Logit
(`dryml.models.statsmodels`). Imports and configuration are passive; fitting and
Expand Down
8 changes: 8 additions & 0 deletions docs/signatures.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,14 @@ binding, discovery, context ownership, wrapper conflicts, and unavailable or
ambiguous authority. Repo and lifecycle errors retain their own types and cleanup
semantics during Mat realization.

For a closed constructor signature, a known unexpected keyword is included in
`SignatureError.slot` and the primary message, including through
`Definition(...).build()`. For example, a misspelled `chlid` keyword reports
`Signature normalization failed: argument binding failed at chlid.` The reason
remains `"argument binding failed"`, argument values are excluded, and the
original Python `TypeError` remains the chained cause. Accepted variadic keyword
names are not labeled as unexpected when another binding requirement fails.

Symbolic Definition failures use `ParameterizationError(ValueError)`. Its
specializations are `UnresolvedDefinitionError` for required resolution,
`ParameterizationLimitError` for bounded traversal or generation work, and
Expand Down
16 changes: 14 additions & 2 deletions src/dryml/core/signatures.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ class SignatureError(TypeError):

Args:
reason: Fixed diagnostic category describing the failed contract.
slot: Optional parameter or return slot path.
slot: Optional parameter or return slot path, including a known
unexpected keyword name when binding a closed constructor signature.

Errors never include caller value representations. Chained lower-level errors
remain available as causes.
Expand Down Expand Up @@ -547,6 +548,8 @@ def prepare_args(self, args: tuple[Any, ...], kwargs: Mapping[str, Any], *, repo

Raises:
SignatureError: If binding, controls, or assertions are invalid.
Unexpected keywords in closed constructor signatures identify
the keyword in the error's slot without rendering its value.
Exception: Propagates explicitly requested Repo metadata failures
without changing their type.

Expand All @@ -566,7 +569,16 @@ def _prepare_args(self, args: tuple[Any, ...], kwargs: Mapping[str, Any],
bound = self.signature.bind(*args, **dict(kwargs))
bound.apply_defaults()
except TypeError as error:
raise SignatureError("argument binding failed") from error
slot = None
if self.constructor and not any(
parameter.kind is inspect.Parameter.VAR_KEYWORD
for parameter in self.signature.parameters.values()
):
slot = next((
name for name in kwargs
if type(name) is str and name not in self.signature.parameters
), None)
raise SignatureError("argument binding failed", slot) from error
controls = _validate_controls(
controls, _selection_keys(self.signature, bound.arguments)
)
Expand Down
58 changes: 58 additions & 0 deletions tests/core/test_signatures.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,64 @@ def __init__(self, value: Ref[ConcreteDefinition]):
assert compile_signature(ConstructorTarget, constructor=True).slots["value"].role == "ref"


def test_definition_build_names_unexpected_constructor_keyword_without_values() -> None:
"""Definition builds expose the typo in the primary error before construction."""

with pytest.raises(SignatureError) as caught:
Definition(SignatureContainer, child=None, chlid="private argument value").build()

assert caught.value.reason == "argument binding failed"
assert caught.value.slot == "chlid"
assert "at chlid" in str(caught.value)
assert "private argument value" not in str(caught.value)
assert isinstance(caught.value.__cause__, TypeError)
assert "chlid" in str(caught.value.__cause__)


def test_constructor_keyword_diagnostic_does_not_render_argument_values() -> None:
"""Finding a constructor typo never invokes a rejected value's repr hook."""

class Unprintable:
def __repr__(self):
pytest.fail("binding diagnostics must not render argument values")

plan = compile_signature(SignatureContainer, constructor=True)
with pytest.raises(SignatureError) as caught:
plan.prepare_args((), {"child": None, "chlid": Unprintable()})

assert caught.value.slot == "chlid"
assert "at chlid" in str(caught.value)


@pytest.mark.parametrize("mode", ("function", "variadic-constructor", "nonstring-key"))
def test_binding_errors_do_not_guess_unexpected_constructor_keywords(mode) -> None:
"""Function calls, accepted variadic keys and malformed keys keep generic errors."""

def target(child=None):
return child

class VariadicConstructor:
def __init__(self, required, **options):
pytest.fail("invalid calls must fail before the constructor runs")

if mode == "function":
plan = compile_signature(target)
kwargs = {"chlid": "private argument value"}
elif mode == "variadic-constructor":
plan = compile_signature(VariadicConstructor, constructor=True)
kwargs = {"valid_extra": "private argument value"}
else:
plan = compile_signature(SignatureContainer, constructor=True)
kwargs = {1: "private argument value"}

with pytest.raises(SignatureError) as caught:
plan.prepare_args((), kwargs)

assert caught.value.reason == "argument binding failed"
assert caught.value.slot is None
assert str(caught.value) == "Signature normalization failed: argument binding failed."


@pytest.mark.parametrize(
"annotation",
(
Expand Down
Loading