You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(validators): name the failing inner item in deep_iterable/deep_mapping errors - #1654
When an inner validator of deep_iterable() or deep_mapping() fails, the error message names the whole attribute instead of the offending member, so on a nested structure you learn that something is wrong but not where:
@attrs.defineclassC:
x=attrs.field(validator=attrs.validators.deep_iterable(
member_validator=attrs.validators.min_len(1),
iterable_validator=None,
))
C(x=[[], [1]])
# ValueError: Length of 'x' must be >= 1: 0# ^^^ no position
The same happens for deep_mapping() with both the key and the value validator:
C(m={"a": 1, "b": "nope"})
# TypeError: ('m' must be <class 'int'> (got 'nope') ...)# ^^^ no key
Root cause
_DeepIterable.__call__ and _DeepMapping.__call__ invoke the member validator directly with the outerAttribute object. An Attribute carries its name in .name, and every built-in validator formats its message from attr.name — so the inner failure can only ever say x / m.
Fix
src/attr/validators.py (+33/−4):
add a tiny _MemberAttribute stand-in that forwards everything (__getattr__) to the parent attribute and overrides only name (→ "x[1]", "m[2]", "m['a']") and __repr__;
_DeepIterable passes it as attr to member_validator;
_DeepMapping builds it once per key from repr(key) and passes it to both key_validator and value_validator.
No public API change: the shim is private (_MemberAttribute), __slots__ is kept, and the object quacks like the attribute it wraps, so third-party validators that read attr.name, attr.type, attr.metadata, … keep working — they just get a more precise name.
After the change:
ValueError: Length of 'x[1]' must be >= 1: 0
TypeError: 'm[2]' must be <class 'str'> (got 2 that is a <class 'int'>) ...
Tests
tests/test_validators.py (+60/−1) — new TestInnerValidatorErrorMessages, 4 cases, one per call site:
test_deep_iterable_names_the_failing_member
test_deep_iterable_names_the_wrongly_typed_member
test_deep_mapping_names_the_failing_value
test_deep_mapping_names_the_failing_key
Each test asserts the exact message produced by the real validators (no mocks) and is expected to fail on main.
Diff: exactly 2 files, +93/−5, 1 commit on top of main. The base and the branch head share the same parent, so the diff is clean with no unrelated churn.
One behavioural change worth reviewing
not_() validates exc_types through deep_iterable, so TestNot_::test_bad_exception_args now reports 'exc_types[0]' instead of 'exc_types'. That assertion is the single line changed in an existing test (+1/−1 in tests/), and it is arguably the improvement this issue asks for — but if you'd rather keep the old wording there, I can gate the shim on an option and update this PR.
Notes
No changelog fragment yet — tell me the format you want (changelog.d/1245.change.md?) and I'll push it.
I'm happy to squash/rebase, drop the _MemberAttribute shim in favour of attrs.evolve-style naming, or split the deep_mapping part into its own PR if you prefer a smaller diff.
Reviewer
@hynek — requesting your review on this PR: it's a 2-file, single-commit fix for #1245 with a red-before-green test for every call site (deep_iterable member, deep_mapping key and value). I'd value your call on the two points above (the exc_types[0] wording and whether you want a changelog fragment), and I'll adjust within the day.
@hynek — requesting your review on this PR. It is a 2-file, +93/−5 change on top of 9d9f94a (main), single commit2529f83.
What it does — deep_iterable() / deep_mapping() now hand the inner validator a private _MemberAttribute shim, so an inner failure names the offending member (x[1], m[2], m['a']) instead of the whole attribute (x, m). No public API change: the shim forwards every attribute access (type, metadata, …) to the real Attribute and overrides only name / __repr__.
Red before green — base commit 9d9f94a with only tests/test_validators.py from this branch applied:
One behavioural change worth your decision — not_() routes exc_types through deep_iterable, so TestNot_::test_bad_exception_args now reports 'exc_types[0]' instead of 'exc_types'. That single assertion is the only pre-existing line this PR touches; it is arguably the improvement #1245 asks for, but say the word and I will gate the shim behind an opt-in so the old wording stays there.
No changelog fragment yet — tell me the changelog.d/ naming you want and I will add it. Happy to squash/rebase or split the deep_mapping half into its own PR if a smaller diff is easier to review.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Closes #1245.
When an inner validator of
deep_iterable()ordeep_mapping()fails, the error message names the whole attribute instead of the offending member, so on a nested structure you learn that something is wrong but not where:The same happens for
deep_mapping()with both the key and the value validator:Root cause
_DeepIterable.__call__and_DeepMapping.__call__invoke the member validator directly with the outerAttributeobject. AnAttributecarries its name in.name, and every built-in validator formats its message fromattr.name— so the inner failure can only ever sayx/m.Fix
src/attr/validators.py(+33/−4):_MemberAttributestand-in that forwards everything (__getattr__) to the parent attribute and overrides onlyname(→"x[1]","m[2]","m['a']") and__repr__;_DeepIterablepasses it asattrtomember_validator;_DeepMappingbuilds it once per key fromrepr(key)and passes it to bothkey_validatorandvalue_validator.No public API change: the shim is private (
_MemberAttribute),__slots__is kept, and the object quacks like the attribute it wraps, so third-party validators that readattr.name,attr.type,attr.metadata, … keep working — they just get a more precise name.After the change:
Tests
tests/test_validators.py(+60/−1) — newTestInnerValidatorErrorMessages, 4 cases, one per call site:test_deep_iterable_names_the_failing_membertest_deep_iterable_names_the_wrongly_typed_membertest_deep_mapping_names_the_failing_valuetest_deep_mapping_names_the_failing_keyEach test asserts the exact message produced by the real validators (no mocks) and is expected to fail on
main.Red before green
Branch base:
9d9f94a043de7fc4012459853a4ed597a5577855(main).On the base commit with only the new tests applied:
On the branch (single commit
2529f830da93e42fd7f6a5d09b4204e95a4ffcd0, tree0634e0d744e7c0f51288023675e210417a8e0c72):Quality gates (on the branch tree)
pytest -qruff check --fix --exit-non-zero-on-fix .All checks passed!— exit 0ruff format --exit-non-zero-on-format .Diff: exactly 2 files,
+93/−5, 1 commit on top ofmain. The base and the branch head share the same parent, so the diff is clean with no unrelated churn.One behavioural change worth reviewing
not_()validatesexc_typesthroughdeep_iterable, soTestNot_::test_bad_exception_argsnow reports'exc_types[0]'instead of'exc_types'. That assertion is the single line changed in an existing test (+1/−1 intests/), and it is arguably the improvement this issue asks for — but if you'd rather keep the old wording there, I can gate the shim on an option and update this PR.Notes
changelog.d/1245.change.md?) and I'll push it._MemberAttributeshim in favour ofattrs.evolve-style naming, or split thedeep_mappingpart into its own PR if you prefer a smaller diff.Reviewer
@hynek — requesting your review on this PR: it's a 2-file, single-commit fix for #1245 with a red-before-green test for every call site (
deep_iterablemember,deep_mappingkey and value). I'd value your call on the two points above (theexc_types[0]wording and whether you want a changelog fragment), and I'll adjust within the day.