.. changelog::
:version: 1.1.0b1
+ .. change::
+ :tags: feature, orm
+ :tickets: 1311
+
+ Added new event :meth:`.AttributeEvents.init_scalar`, as well
+ as a new example suite illustrating its use. This event can be used
+ to provide a Core-generated default value to a Python-side attribute
+ before the object is persisted.
+
+ .. seealso::
+
+ :ref:`change_1311`
+
.. change::
:tags: feature, postgresql
:tickets: 3720
:ticket:`2677`
+.. _change_1311:
+
+New init_scalar() event intercepts default values at ORM level
+--------------------------------------------------------------
+
+The ORM produces a value of ``None`` when an attribute that has not been
+set is first accessed, for a non-persistent object::
+
+ >>> obj = MyObj()
+ >>> obj.some_value
+ None
+
+There's a use case for this in-Python value to correspond to that of a
+Core-generated default value, even before the object is persisted.
+To suit this use case a new event :meth:`.AttributeEvents.init_scalar`
+is added. The new example ``active_column_defaults.py`` at
+:ref:`examples_instrumentation` illustrates a sample use, so the effect
+can instead be::
+
+ >>> obj = MyObj()
+ >>> obj.some_value
+ "my default"
+
+:ticket:`1311`
+
.. _change_3499:
Changes regarding "unhashable" types
"""
-Two examples illustrating modifications to SQLAlchemy's attribute management
+Examples illustrating modifications to SQLAlchemy's attribute management
system.
.. autosource::
--- /dev/null
+"""Illustrates use of the :meth:`.AttributeEvents.init_scalar`
+event, in conjunction with Core column defaults to provide
+ORM objects that automatically produce the default value
+when an un-set attribute is accessed.
+
+"""
+
+from sqlalchemy import event
+
+
+def configure_listener(mapper, class_):
+ """Establish attribute setters for every default-holding column on the
+ given mapper."""
+
+ # iterate through ColumnProperty objects
+ for col_attr in mapper.column_attrs:
+
+ # look at the Column mapped by the ColumnProperty
+ # (we look at the first column in the less common case
+ # of a property mapped to multiple columns at once)
+ column = col_attr.columns[0]
+
+ # if the Column has a "default", set up a listener
+ if column.default is not None:
+ default_listener(col_attr, column.default)
+
+
+def default_listener(col_attr, default):
+ """Establish a default-setting listener.
+
+ Given a class_, attrname, and a :class:`.DefaultGenerator` instance.
+ The default generator should be a :class:`.ColumnDefault` object with a
+ plain Python value or callable default; otherwise, the appropriate behavior
+ for SQL functions and defaults should be determined here by the
+ user integrating this feature.
+
+ """
+ @event.listens_for(col_attr, "init_scalar", retval=True, propagate=True)
+ def init_scalar(target, value, dict_):
+
+ if default.is_callable:
+ # the callable of ColumnDefault always accepts a context
+ # argument; we can pass it as None here.
+ value = default.arg(None)
+ elif default.is_scalar:
+ value = default.arg
+ else:
+ # default is a Sequence, a SQL expression, server
+ # side default generator, or other non-Python-evaluable
+ # object. The feature here can't easily support this. This
+ # can be made to return None, rather than raising,
+ # or can procure a connection from an Engine
+ # or Session and actually run the SQL, if desired.
+ raise NotImplementedError(
+ "Can't invoke pre-default for a SQL-level column default")
+
+ # set the value in the given dict_; this won't emit any further
+ # attribute set events or create attribute "history", but the value
+ # will be used in the INSERT statement
+ dict_[col_attr.key] = value
+
+ # return the value as well
+ return value
+
+
+if __name__ == '__main__':
+
+ from sqlalchemy import Column, Integer, DateTime, create_engine
+ from sqlalchemy.orm import Session
+ from sqlalchemy.ext.declarative import declarative_base
+ import datetime
+
+ Base = declarative_base()
+
+ event.listen(Base, 'mapper_configured', configure_listener, propagate=True)
+
+ class Widget(Base):
+ __tablename__ = 'widget'
+
+ id = Column(Integer, primary_key=True)
+
+ radius = Column(Integer, default=30)
+ timestamp = Column(DateTime, default=datetime.datetime.now)
+
+ e = create_engine("sqlite://", echo=True)
+ Base.metadata.create_all(e)
+
+ w1 = Widget()
+
+ # not persisted at all, default values are present the moment
+ # we access them
+ assert w1.radius == 30
+ current_time = w1.timestamp
+ assert (
+ current_time > datetime.datetime.now() - datetime.timedelta(seconds=5)
+ )
+
+ # persist
+ sess = Session(e)
+ sess.add(w1)
+ sess.commit()
+
+ # data is persisted. The timestamp is also the one we generated above;
+ # e.g. the default wasn't re-invoked later.
+ assert (
+ sess.query(Widget.radius, Widget.timestamp).first() ==
+ (30, current_time)
+ )
def initialize(self, state, dict_):
"""Initialize the given state's attribute with an empty value."""
- # As of 1.0, we don't actually set a value in
- # dict_. This is so that the state of the object does not get
- # modified without emitting the appropriate events.
+ value = None
+ for fn in self.dispatch.init_scalar:
+ ret = fn(state, value, dict_)
+ if ret is not ATTR_EMPTY:
+ value = ret
-
- return None
+ return value
def get(self, state, dict_, passive=PASSIVE_OFF):
"""Retrieve a value from the given object.
"""
+ def init_scalar(self, target, value, dict_):
+ """Receive a scalar "init" event.
+
+ This event is invoked when an uninitialized, unpersisted scalar
+ attribute is accessed. A value of ``None`` is typically returned
+ in this case; no changes are made to the object's state.
+
+ The event handler can alter this behavior in two ways.
+ One is that a value other than ``None`` may be returned. The other
+ is that the value may be established as part of the object's state,
+ which will also have the effect that it is persisted.
+
+ Typical use is to establish a specific default value of an attribute
+ upon access::
+
+ SOME_CONSTANT = 3.1415926
+
+ @event.listens_for(
+ MyClass.some_attribute, "init_scalar",
+ retval=True, propagate=True)
+ def _init_some_attribute(target, dict_, value):
+ dict_['some_attribute'] = SOME_CONSTANT
+ return SOME_CONSTANT
+
+ Above, we initialize the attribute ``MyClass.some_attribute`` to the
+ value of ``SOME_CONSTANT``. The above code includes the following
+ features:
+
+ * By setting the value ``SOME_CONSTANT`` in the given ``dict_``,
+ we indicate that the value is to be persisted to the database.
+ **The given value is only persisted to the database if we
+ explicitly associate it with the object**. The ``dict_`` given
+ is the ``__dict__`` element of the mapped object, assuming the
+ default attribute instrumentation system is in place.
+
+ * By establishing the ``retval=True`` flag, the value we return
+ from the function will be returned by the attribute getter.
+ Without this flag, the event is assumed to be a passive observer
+ and the return value of our function is ignored.
+
+ * The ``propagate=True`` flag is significant if the mapped class
+ includes inheriting subclasses, which would also make use of this
+ event listener. Without this flag, an inheriting subclass will
+ not use our event handler.
+
+ When we establish the value in the given dictionary, the value will
+ be used in the INSERT statement established by the unit of work.
+ Normally, the default returned value of ``None`` is not established as
+ part of the object, to avoid the issue of mutations occurring to the
+ object in response to a normally passive "get" operation, and also
+ sidesteps the issue of whether or not the :meth:`.AttributeEvents.set`
+ event should be awkwardly fired off during an attribute access
+ operation. This does not impact the INSERT operation since the
+ ``None`` value matches the value of ``NULL`` that goes into the
+ database in any case; note that ``None`` is skipped during the INSERT
+ to ensure that column and SQL-level default functions can fire off.
+
+ The attribute set event :meth:`.AttributeEvents.set` as well as the
+ related validation feature provided by :obj:`.orm.validates` is
+ **not** invoked when we apply our value to the given ``dict_``. To
+ have these events to invoke in response to our newly generated
+ value, apply the value to the given object as a normal attribute
+ set operation::
+
+ SOME_CONSTANT = 3.1415926
+
+ @event.listens_for(
+ MyClass.some_attribute, "init_scalar",
+ retval=True, propagate=True)
+ def _init_some_attribute(target, dict_, value):
+ # will also fire off attribute set events
+ target.some_attribute = SOME_CONSTANT
+ return SOME_CONSTANT
+
+ When multiple listeners are set up, the generation of the value
+ is "chained" from one listener to the next by passing the value
+ returned by the previous listener that specifies ``retval=True``
+ as the ``value`` argument of the next listener.
+
+ The :meth:`.AttributeEvents.init_scalar` event may be used to
+ extract values from the default values and/or callables established on
+ mapped :class:`.Column` objects. See the "active column defaults"
+ example in :ref:`examples_instrumentation` for an example of this.
+
+ .. versionadded:: 1.1
+
+ :param target: the object instance receiving the event.
+ If the listener is registered with ``raw=True``, this will
+ be the :class:`.InstanceState` object.
+ :param value: the value that is to be returned before this event
+ listener were invoked. This value begins as the value ``None``,
+ however will be the return value of the previous event handler
+ function if multiple listeners are present.
+ :param dict_: the attribute dictionary of this mapped object.
+ This is normally the ``__dict__`` of the object, but in all cases
+ represents the destination that the attribute system uses to get
+ at the actual value of this attribute. Placing the value in this
+ dictionary has the effect that the value will be used in the
+ INSERT statement generated by the unit of work.
+
+
+ .. seealso::
+
+ :ref:`examples_instrumentation` - see the
+ ``active_column_defaults.py`` example.
+
+ """
+
def init_collection(self, target, collection, collection_adapter):
"""Receive a 'collection init' event.