From c3ec789ecdd4e33786b8ac0f1ccdf155c15eda9d Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 8 Feb 2018 09:16:39 -0500 Subject: [PATCH] Add initiator argument to set_attribute Added new argument :paramref:`.attributes.set_attribute.inititator` to the :func:`.attributes.set_attribute` function, allowing an event token received from a listener function to be propagated to subsequent set events. Change-Id: I6ede21e42153026ab46a1d2ec33aa3f999db98e2 --- .../changelog/unreleased_12/add_initiator.rst | 7 ++++ lib/sqlalchemy/orm/attributes.py | 19 ++++++++-- test/orm/test_attributes.py | 36 +++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 doc/build/changelog/unreleased_12/add_initiator.rst diff --git a/doc/build/changelog/unreleased_12/add_initiator.rst b/doc/build/changelog/unreleased_12/add_initiator.rst new file mode 100644 index 0000000000..a37f9ae848 --- /dev/null +++ b/doc/build/changelog/unreleased_12/add_initiator.rst @@ -0,0 +1,7 @@ +.. change:: + :tags: feature, orm + + Added new argument :paramref:`.attributes.set_attribute.inititator` + to the :func:`.attributes.set_attribute` function, allowing an + event token received from a listener function to be propagated + to subsequent set events. diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index b175297ace..e9227362e7 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -1592,7 +1592,7 @@ def set_committed_value(instance, key, value): state.manager[key].impl.set_committed_value(state, dict_, value) -def set_attribute(instance, key, value): +def set_attribute(instance, key, value, initiator=None): """Set the value of an attribute, firing history events. This function may be used regardless of instrumentation @@ -1601,9 +1601,24 @@ def set_attribute(instance, key, value): of this method to establish attribute state as understood by SQLAlchemy. + :param instance: the object that will be modified + + :param key: string name of the attribute + + :param value: value to assign + + :param initiator: an instance of :class:`.Event` that would have + been propagated from a previous event listener. This argument + is used when the :func:`.set_attribute` function is being used within + an existing event listening function where an :class:`.Event` object + is being supplied; the object may be used to track the origin of the + chain of events. + + .. versionadded:: 1.2.3 + """ state, dict_ = instance_state(instance), instance_dict(instance) - state.manager[key].impl.set(state, dict_, value, None) + state.manager[key].impl.set(state, dict_, value, initiator) def get_attribute(instance, key): diff --git a/test/orm/test_attributes.py b/test/orm/test_attributes.py index 4646a010ad..12c9dddb9c 100644 --- a/test/orm/test_attributes.py +++ b/test/orm/test_attributes.py @@ -1014,6 +1014,42 @@ class UtilTest(fixtures.ORMTest): attributes.del_attribute(f1, "coll") assert "coll" not in f1.__dict__ + def test_initiator_arg(self): + class Foo(object): + pass + + class Bar(object): + pass + + instrumentation.register_class(Foo) + instrumentation.register_class(Bar) + attributes.register_attribute( + Foo, "a", uselist=False, useobject=False) + attributes.register_attribute( + Bar, "b", uselist=False, useobject=False) + + @event.listens_for(Foo.a, "set") + def sync_a(target, value, oldvalue, initiator): + parentclass = initiator.parent_token.class_ + if parentclass is Foo: + attributes.set_attribute(target.bar, "b", value, initiator) + + @event.listens_for(Bar.b, "set") + def sync_b(target, value, oldvalue, initiator): + parentclass = initiator.parent_token.class_ + if parentclass is Bar: + attributes.set_attribute(target.foo, "a", value, initiator) + + f1 = Foo() + b1 = Bar() + f1.bar = b1 + b1.foo = f1 + + f1.a = 'x' + eq_(b1.b, 'x') + b1.b = 'y' + eq_(f1.a, 'y') + class BackrefTest(fixtures.ORMTest): -- 2.47.2