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