…uteError
Validators that reach for string methods on the value -- uuid via
UUID(value) -> value.replace, email/hostname via value.count, cron via
value.strip -- raise AttributeError when given a non-string (int, float,
bool, list, dict). The validator decorator only converted (ValueError,
TypeError, UnicodeError) to ValidationError, so AttributeError escaped
uncaught: a caller passing untrusted or unknown-typed data to a validator
got a crash instead of the documented True/ValidationError result, and
inconsistently with the many validators that already return ValidationError
for wrong-typed input.
Add AttributeError to the decorator's caught set so this whole class of
validators handles non-string input uniformly.
Summary
Several validators leak an uncaught AttributeError on non-string input instead of returning ValidationError:
This is inconsistent with the many validators that already return ValidationError for wrong-typed input (domain, slug, md5, url, iban, mac_address, …). Since a validator's job is to vet untrusted/unknown input, a caller reasonably passes a value of unknown type and expects True/ValidationError — not a crash.
Cause
These validators reach for a string method on the value (UUID(value) → value.replace(...), value.count(...), value.strip()), which raises AttributeError for an int/float/bool/list/dict. The @validator decorator in utils.py converts (ValueError, TypeError, UnicodeError) into ValidationError, but AttributeError isn't in that set, so it escapes.
Fix
Add AttributeError to the decorator's caught set. AttributeError from calling a string method on a non-string is squarely the "invalid input" category the decorator already handles for TypeError/ValueError, so this makes the whole class of validators handle non-string input uniformly — at the one place that defines the contract — and guards future validators from the same gap.
Added a cross-validator regression test (test_returns_validation_error_on_non_string_input, uuid/email/hostname/cron × int/float/bool/list/dict). Full suite: 915 passed (was 895). ruff check / ruff format --check clean.
Note / scope
I scoped this to the AttributeError class. Two validators (es_doi, es_nie) leak KeyError on some inputs via a different code path; I left those out deliberately, since catching KeyError globally in the decorator would risk masking genuine logic errors. Happy to address them separately if you'd like.
This pull request was prepared with the assistance of AI, under my direction and review.