1"""Illustrates use of the :meth:`.AttributeEvents.init_scalar` 2event, in conjunction with Core column defaults to provide 3ORM objects that automatically produce the default value 4when an un-set attribute is accessed. 5 6""" 7 8from sqlalchemy import event 9 10 11def configure_listener(mapper, class_): 12 """Establish attribute setters for every default-holding column on the 13 given mapper.""" 14 15 # iterate through ColumnProperty objects 16 for col_attr in mapper.column_attrs: 17 18 # look at the Column mapped by the ColumnProperty 19 # (we look at the first column in the less common case 20 # of a property mapped to multiple columns at once) 21 column = col_attr.columns[0] 22 23 # if the Column has a "default", set up a listener 24 if column.default is not None: 25 default_listener(col_attr, column.default) 26 27 28def default_listener(col_attr, default): 29 """Establish a default-setting listener. 30 31 Given a class attribute and a :class:`.DefaultGenerator` instance. 32 The default generator should be a :class:`.ColumnDefault` object with a 33 plain Python value or callable default; otherwise, the appropriate behavior 34 for SQL functions and defaults should be determined here by the 35 user integrating this feature. 36 37 """ 38 39 @event.listens_for(col_attr, "init_scalar", retval=True, propagate=True) 40 def init_scalar(target, value, dict_): 41 42 if default.is_callable: 43 # the callable of ColumnDefault always accepts a context 44 # argument; we can pass it as None here. 45 value = default.arg(None) 46 elif default.is_scalar: 47 value = default.arg 48 else: 49 # default is a Sequence, a SQL expression, server 50 # side default generator, or other non-Python-evaluable 51 # object. The feature here can't easily support this. This 52 # can be made to return None, rather than raising, 53 # or can procure a connection from an Engine 54 # or Session and actually run the SQL, if desired. 55 raise NotImplementedError( 56 "Can't invoke pre-default for a SQL-level column default" 57 ) 58 59 # set the value in the given dict_; this won't emit any further 60 # attribute set events or create attribute "history", but the value 61 # will be used in the INSERT statement 62 dict_[col_attr.key] = value 63 64 # return the value as well 65 return value 66 67 68if __name__ == "__main__": 69 70 from sqlalchemy import Column, Integer, DateTime, create_engine 71 from sqlalchemy.orm import Session 72 from sqlalchemy.ext.declarative import declarative_base 73 import datetime 74 75 Base = declarative_base() 76 77 event.listen(Base, "mapper_configured", configure_listener, propagate=True) 78 79 class Widget(Base): 80 __tablename__ = "widget" 81 82 id = Column(Integer, primary_key=True) 83 84 radius = Column(Integer, default=30) 85 timestamp = Column(DateTime, default=datetime.datetime.now) 86 87 e = create_engine("sqlite://", echo=True) 88 Base.metadata.create_all(e) 89 90 w1 = Widget() 91 92 # not persisted at all, default values are present the moment 93 # we access them 94 assert w1.radius == 30 95 96 # this line will invoke the datetime.now() function, and establish 97 # its return value upon the w1 instance, such that the 98 # Column-level default for the "timestamp" column will no longer fire 99 # off. 100 current_time = w1.timestamp 101 assert current_time > datetime.datetime.now() - datetime.timedelta( 102 seconds=5 103 ) 104 105 # persist 106 sess = Session(e) 107 sess.add(w1) 108 sess.commit() 109 110 # data is persisted. The timestamp is also the one we generated above; 111 # e.g. the default wasn't re-invoked later. 112 assert sess.query(Widget.radius, Widget.timestamp).first() == ( 113 30, 114 current_time, 115 ) 116