root / elixir / trunk / elixir / properties.py

Revision 484, 7.9 kB (checked in by ged, 5 months ago)

update all web links

Line 
1'''
2This module provides support for defining properties on your entities. It both
3provides, the `Property` class which acts as a building block for common
4properties such as fields and relationships (for those, please consult the
5corresponding modules), but also provides some more specialized properties,
6such as `ColumnProperty` and `Synonym`. It also provides the GenericProperty
7class which allows you to wrap any SQLAlchemy property, and its DSL-syntax
8equivalent: has_property_.
9
10`has_property`
11--------------
12The ``has_property`` statement allows you to define properties which rely on
13their entity's table (and columns) being defined before they can be declared
14themselves. The `has_property` statement takes two arguments: first the name of
15the property to be defined and second a function (often given as an anonymous
16lambda) taking one argument and returning the desired SQLAlchemy property. That
17function will be called whenever the entity table is completely defined, and
18will be given the .c attribute of the entity as argument (as a way to access
19the entity columns).
20
21Here is a quick example of how to use ``has_property``.
22
23.. sourcecode:: python
24
25    class OrderLine(Entity):
26        has_field('quantity', Float)
27        has_field('unit_price', Float)
28        has_property('price',
29                     lambda c: column_property(
30                         (c.quantity * c.unit_price).label('price')))
31'''
32
33from elixir.statements import PropertyStatement
34from sqlalchemy.orm import column_property, synonym
35
36__doc_all__ = ['EntityBuilder', 'Property', 'GenericProperty',
37               'ColumnProperty']
38
39class EntityBuilder(object):
40    '''
41    Abstract base class for all entity builders. An Entity builder is a class
42    of objects which can be added to an Entity (usually by using special
43    properties or statements) to "build" that entity. Building an entity,
44    meaning to add columns to its "main" table, create other tables, add
45    properties to its mapper, ... To do so an EntityBuilder must override the
46    corresponding method(s). This is to ensure the different operations happen
47    in the correct order (for example, that the table is fully created before
48    the mapper that use it is defined).
49    '''
50    def create_pk_cols(self):
51        pass
52
53    def create_non_pk_cols(self):
54        pass
55
56    def before_table(self):
57        pass
58
59    def create_tables(self):
60        '''
61        Subclasses may override this method to create tables.
62        '''
63
64    def after_table(self):
65        pass
66
67    def create_properties(self):
68        '''
69        Subclasses may override this method to add properties to the involved
70        entity.
71        '''
72
73    def before_mapper(self):
74        pass
75
76    def after_mapper(self):
77        pass
78
79    def finalize(self):
80        pass
81
82    # helper methods
83    def add_table_column(self, column):
84        self.entity._descriptor.add_column(column)
85
86    def add_mapper_property(self, name, prop):
87        self.entity._descriptor.add_property(name, prop)
88
89    def add_mapper_extension(self, ext):
90        self.entity._descriptor.add_mapper_extension(ext)
91
92
93class CounterMeta(type):
94    '''
95    A simple meta class which adds a ``_counter`` attribute to the instances of
96    the classes it is used on. This counter is simply incremented for each new
97    instance.
98    '''
99    counter = 0
100
101    def __call__(self, *args, **kwargs):
102        instance = type.__call__(self, *args, **kwargs)
103        instance._counter = CounterMeta.counter
104        CounterMeta.counter += 1
105        return instance
106
107
108class Property(EntityBuilder):
109    '''
110    Abstract base class for all properties of an Entity.
111    '''
112    __metaclass__ = CounterMeta
113
114    def __init__(self, *args, **kwargs):
115        self.entity = None
116        self.name = None
117
118    def attach(self, entity, name):
119        """Attach this property to its entity, using 'name' as name.
120
121        Properties will be attached in the order they were declared.
122        """
123        self.entity = entity
124        self.name = name
125
126        # register this property as a builder
127        entity._descriptor.builders.append(self)
128
129    def __repr__(self):
130        return "Property(%s, %s)" % (self.name, self.entity)
131
132
133class GenericProperty(Property):
134    '''
135    Generic catch-all class to wrap an SQLAlchemy property.
136
137    .. sourcecode:: python
138
139        class OrderLine(Entity):
140            quantity = Field(Float)
141            unit_price = Field(Numeric)
142            price = GenericProperty(lambda c: column_property(
143                             (c.quantity * c.unit_price).label('price')))
144    '''
145
146    def __init__(self, prop, *args, **kwargs):
147        super(GenericProperty, self).__init__(*args, **kwargs)
148        self.prop = prop
149        #XXX: move this to Property?
150        self.args = args
151        self.kwargs = kwargs
152
153    def create_properties(self):
154        if hasattr(self.prop, '__call__'):
155            prop_value = self.prop(self.entity.table.c)
156        else:
157            prop_value = self.prop
158        prop_value = self.evaluate_property(prop_value)
159        self.add_mapper_property(self.name, prop_value)
160
161    def evaluate_property(self, prop):
162        if self.args or self.kwargs:
163            raise Exception('superfluous arguments passed to GenericProperty')
164        return prop
165
166
167class ColumnProperty(GenericProperty):
168    '''
169    A specialized form of the GenericProperty to generate SQLAlchemy
170    ``column_property``'s.
171
172    It takes a function (often given as an anonymous lambda) as its first
173    argument. Other arguments and keyword arguments are forwarded to the
174    column_property construct. That first-argument function must accept exactly
175    one argument and must return the desired (scalar-returning) SQLAlchemy
176    ClauseElement.
177
178    The function will be called whenever the entity table is completely
179    defined, and will be given
180    the .c attribute of the table of the entity as argument (as a way to
181    access the entity columns). The ColumnProperty will first wrap your
182    ClauseElement in an
183    "empty" label (ie it will be labelled automatically during queries),
184    then wrap that in a column_property.
185
186    .. sourcecode:: python
187
188        class OrderLine(Entity):
189            quantity = Field(Float)
190            unit_price = Field(Numeric)
191            price = ColumnProperty(lambda c: c.quantity * c.unit_price,
192                                   deferred=True)
193
194    Please look at the `corresponding SQLAlchemy
195    documentation <http://www.sqlalchemy.org/docs/05/mappers.html
196    #sql-expressions-as-mapped-attributes>`_ for details.
197    '''
198
199    def evaluate_property(self, prop):
200        return column_property(prop.label(None), *self.args, **self.kwargs)
201
202
203class Synonym(GenericProperty):
204    '''
205    This class represents a synonym property of another property (column, ...)
206    of an entity.  As opposed to the `synonym` kwarg to the Field class (which
207    share the same goal), this class can be used to define a synonym of a
208    property defined in a parent class (of the current class). On the other
209    hand, it cannot define a synonym for the purpose of using a standard python
210    property in queries. See the Field class for details on that usage.
211
212    .. sourcecode:: python
213
214    class Person(Entity):
215        name = Field(String(30))
216        primary_email = Field(String(100))
217        email_address = Synonym('primary_email')
218
219    class User(Person):
220        user_name = Synonym('name')
221        password = Field(String(20))
222    '''
223
224    def evaluate_property(self, prop):
225        return synonym(prop, *self.args, **self.kwargs)
226
227#class Composite(GenericProperty):
228#    def __init__(self, prop):
229#        super(GenericProperty, self).__init__()
230#        self.prop = prop
231
232#    def evaluate_property(self, prop):
233#        return composite(prop.label(self.name))
234
235#start = Composite(Point, lambda c: (c.x1, c.y1))
236
237#mapper(Vertex, vertices, properties={
238#    'start':composite(Point, vertices.c.x1, vertices.c.y1),
239#    'end':composite(Point, vertices.c.x2, vertices.c.y2)
240#})
241
242
243has_property = PropertyStatement(GenericProperty)
Note: See TracBrowser for help on using the browser.