ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-AUTOBUILDSPHINXOPTS = -T .
+AUTOBUILDSPHINXOPTS = -T -j auto .
.PHONY: help clean html autobuild dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest dist-html site-mako gettext
--- /dev/null
+.. change::
+ :tags: usecase, orm
+ :tickets: 2943
+
+ When a subclass overrides a :func:`_orm.validates` method using the
+ same method name as the parent class, only the subclass validator is
+ now invoked for instances of the subclass. The subclass validator
+ may call ``super()`` to also invoke the parent class validator.
+ Previously, the parent validator was always used regardless of
+ whether the subclass provided an override. Pull request courtesy
+ Indivar Mishra.
+
+ .. seealso::
+
+ :ref:`validators_subclass_override`
.. autofunction:: validates
+.. _validators_subclass_override:
+
+Overriding Validators in Subclasses
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. versionadded:: 2.1
+
+A subclass may override a validator defined on its parent class by
+applying the :func:`.validates` decorator to a method that uses the same
+name as the parent's validator. When this is the case, only the subclass
+validator is invoked for instances of the subclass; the parent's
+validator is **not** called automatically.
+
+This allows the subclass to fully replace the parent's validation
+behavior. If the subclass wishes to combine its own behavior with that
+of the parent, it may call ``super()`` within the overriding method to
+also invoke the parent validator::
+
+ from sqlalchemy.orm import validates
+
+
+ class User(Base):
+ __tablename__ = "user"
+
+ id = mapped_column(Integer, primary_key=True)
+ email = mapped_column(String)
+
+ @validates("email")
+ def validate_email(self, key, address):
+ if "@" not in address:
+ raise ValueError("failed simple email validation")
+ return address
+
+
+ class VerifiedUser(User):
+ @validates("email")
+ def validate_email(self, key, address):
+ # call the parent validator first
+ address = super().validate_email(key, address)
+ if not address.endswith("@example.com"):
+ raise ValueError("only example.com addresses allowed")
+ return address
+
+Above, assigning to ``VerifiedUser.email`` invokes
+``VerifiedUser.validate_email`` only; the call to ``super()`` is what
+causes ``User.validate_email`` to run as well. Without that
+``super()`` call, the parent's validator would be skipped entirely.
+
+.. versionchanged:: 2.1
+
+ A subclass overriding a parent's validator using the same method name
+ now replaces the parent validator rather than the parent validator
+ running unconditionally. Previously, the parent validator would
+ always run even when the subclass provided an override, which made it
+ difficult for a subclass to alter or replace the parent's validation
+ behavior.
+
Using Custom Datatypes at the Core Level
-----------------------------------------
modify or replace the value before proceeding. The function should
otherwise return the given value.
+ When a subclass overrides a validator for the same attribute using
+ the same method name, only the subclass validator is invoked. The
+ subclass validator may call ``super()`` to also invoke the parent
+ class validator.
+
+ A subclass that overrides a validator using the same method name
+ as the parent now replaces the parent validator entirely, rather
+ than the parent validator being invoked unconditionally. The
+ subclass validator may opt in to the parent's behavior by calling
+ ``super()``. See :ref:`validators_subclass_override` for
+ background on this change.
+
+ .. versionchanged:: 2.1 Added support for overriding of validators
+ on subclasses.
+
Note that a validator for a collection **cannot** issue a load of that
collection within the validation routine - this usage raises
an assertion to avoid recursion overflows. This is a reentrant
:ref:`simple_validators` - usage examples for :func:`.validates`
+ :ref:`validators_subclass_override` - overriding validators in
+ subclasses
+
"""
def wrap(fn: _Fn) -> _Fn:
default_scalar_value=None,
**kw,
):
- listen_hooks = []
+
+ # event-hook registration functions organized into "pre validate" and "post
+ # validate" collections. Each hook registration function generates a new
+ # AttributeEvents registration for this attribute. What we are controlling
+ # here is the order in which these attribute events are established. Hook
+ # registration functions invoked in the order of first pre-validate, then
+ # user-specific validation i.e. `@validates`, then post-validate.
+ # within `@validates` we are scanning for hooks from child-most classes
+ # first (this suits the feature added in #2943).
+ pre_validate_hooks = []
+ post_validate_hooks = []
uselist = useobject and prop.uselist
if useobject and prop.single_parent:
- listen_hooks.append(_single_parent_validator)
-
- if prop.key in prop.parent.validators:
- fn, opts = prop.parent.validators[prop.key]
- listen_hooks.append(
- lambda desc, prop: orm_util._validator_events(
- desc, prop.key, fn, **opts
- )
- )
+ pre_validate_hooks.append(_single_parent_validator)
if useobject:
- listen_hooks.append(unitofwork._track_cascade_events)
+ post_validate_hooks.append(unitofwork._track_cascade_events)
# need to assemble backref listeners
# after the singleparentvalidator, mapper validator
if useobject:
backref = prop.back_populates
if backref and prop._effective_sync_backref:
- listen_hooks.append(
+ post_validate_hooks.append(
lambda desc, prop: attributes._backref_listeners(
desc, backref, uselist
)
**kw,
)
- for hook in listen_hooks:
+ for hook in pre_validate_hooks:
+ hook(desc, prop)
+
+ for super_m in m.iterate_to_root():
+ if prop.key in super_m.validators:
+ fn, opts = super_m.validators[prop.key]
+ orm_util._validator_events(desc, prop.key, fn, **opts)
+ break
+
+ for hook in post_validate_hooks:
hook(desc, prop)
from unittest.mock import call
from unittest.mock import Mock
+from sqlalchemy import Column
from sqlalchemy import exc
+from sqlalchemy import Integer
+from sqlalchemy import String
from sqlalchemy import testing
from sqlalchemy.orm import collections
+from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import validates
from sqlalchemy.testing import assert_raises
from sqlalchemy.testing import assert_raises_message
from sqlalchemy.testing import eq_
+from sqlalchemy.testing import expect_raises_message
from sqlalchemy.testing import ne_
from sqlalchemy.testing.entities import ComparableEntity
from sqlalchemy.testing.fixtures import fixture_session
call("user", User(addresses=[])),
],
)
+
+ def _inheritance_override_classes(self):
+ Base = declarative_base()
+
+ class A(Base):
+ __tablename__ = "a"
+ id = Column(Integer, primary_key=True)
+ data = Column(String)
+ foo = Column(String)
+
+ @validates("data")
+ def validate_data(self, key, value):
+ return "Call from A : " + value
+
+ @validates("foo")
+ def validate_foo(self, key, value):
+ ne_(value, "exclude for A")
+ return value
+
+ class B(A):
+ foo2 = Column(String)
+ bar = Column(String)
+
+ @validates("data")
+ def validate_data(self, key, value):
+ return "Call from B : " + value
+
+ @validates("foo")
+ def validate_foo(self, key, value):
+ value = super().validate_foo(key, value)
+ ne_(value, "exclude for B")
+ return value
+
+ @validates("foo2", "bar")
+ def validate_foobar(self, key, value):
+ if key == "foo2":
+ return value + "_"
+ return "_" + value
+
+ class C(B):
+ @validates("foo2", "bar")
+ def validate_foobar(self, key, value):
+ if key == "foo2":
+ return value + "-"
+ return "-" + value
+
+ return A, B, C
+
+ def test_validator_override_parent(self):
+ A, B, C = self._inheritance_override_classes()
+
+ obj = A(data="ed")
+ eq_(obj.data, "Call from A : ed")
+ with expect_raises_message(AssertionError, "exclude for A"):
+ obj.foo = "exclude for A"
+ obj.foo = "exclude for B"
+
+ def test_validator_override_subclass_replaces_parent(self):
+ A, B, C = self._inheritance_override_classes()
+
+ obj = B(data="ed")
+ eq_(obj.data, "Call from B : ed")
+
+ def test_validator_override_subclass_calls_super(self):
+ A, B, C = self._inheritance_override_classes()
+
+ obj = B(data="ed")
+ with expect_raises_message(AssertionError, "exclude for A"):
+ obj.foo = "exclude for A"
+ with expect_raises_message(AssertionError, "exclude for B"):
+ obj.foo = "exclude for B"
+ obj.foo = "Some other value"
+
+ def test_validator_override_subclass_multi_attribute(self):
+ A, B, C = self._inheritance_override_classes()
+
+ obj = B(data="ed")
+ obj.foo2 = "foo"
+ obj.bar = "bar"
+ eq_(obj.foo2, "foo_")
+ eq_(obj.bar, "_bar")
+
+ def test_validator_override_grandchild_replaces_parent(self):
+ A, B, C = self._inheritance_override_classes()
+
+ obj = C(data="ed")
+ obj.foo2 = "foo"
+ obj.bar = "bar"
+ eq_(obj.foo2, "foo-")
+ eq_(obj.bar, "-bar")