From 98a742e77ba79b9a4d56a3f3c621b572f940519a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 13:03:54 +0000 Subject: [PATCH 1/2] Initial plan From ae27f5a7cd65116e0b389683190406d52d253c60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 13:09:17 +0000 Subject: [PATCH 2/2] Fix Python 3.9 annotation inheritance bug in _get_class_annotations Co-authored-by: harumaki4649 <83683593+harumaki4649@users.noreply.github.com> --- src/validkit/validator.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/validkit/validator.py b/src/validkit/validator.py index 9dc2748..8fb594b 100644 --- a/src/validkit/validator.py +++ b/src/validkit/validator.py @@ -41,15 +41,35 @@ def _get_class_annotations(schema: type) -> Dict[str, Any]: - """Return annotations declared on *schema*, including lazily exposed ones.""" + """Return annotations declared directly on *schema* (not inherited from base classes). + + Prefers ``schema.__dict__["__annotations__"]`` to avoid Python 3.9's MRO traversal + behavior where ``getattr(cls, "__annotations__")`` returns a *parent* class's dict + when the child declares no annotations of its own. + + The ``getattr`` fallback is kept only for metaclass-defined ``__annotations__`` + properties (lazily computed annotations). In that case the returned mapping is a + freshly constructed object that cannot be ``is``-identical to any base class's + annotations dict, so the identity guard below correctly passes it through. + """ own_annotations = schema.__dict__.get("__annotations__") if isinstance(own_annotations, Mapping): return dict(own_annotations) + # Fallback for metaclass-defined __annotations__ properties. dynamic_annotations = getattr(schema, "__annotations__", None) - if isinstance(dynamic_annotations, Mapping): - return dict(dynamic_annotations) - return {} + if not isinstance(dynamic_annotations, Mapping): + return {} + + # In Python 3.9, getattr traverses the class MRO and may return a parent + # class's __annotations__ dict as-is (same object identity). If the result + # is identical to any base class's own annotations, treat it as inherited and + # return an empty dict to avoid incorrectly pulling in parent-class fields. + for base in schema.__mro__[1:]: + if base.__dict__.get("__annotations__") is dynamic_annotations: + return {} + + return dict(dynamic_annotations) def _is_class_schema(schema: Any) -> bool: