root / elixir / trunk / elixir / properties.py @ 327

Revision 327, 7.2 kB (checked in by ged, 6 years ago)

- Fixed inheritance with autoloaded entities: when using autoload, we

shouldn't try to add columns to the table (closes tickets #41 and #43).

- Fixed ColumnProperty to work with latest version of SQLAlchemy (O.4.5 and

later)

- Added AUTHORS list. If you are missing from this list, don't hesitate to

contact me.

- Merged test_autoload_nopk into test_autoload

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    #XXX: add helper methods: add_property, etc... here?
51    # either in addition to in EntityDescriptor or instead of there.
52    def create_pk_cols(self):
53        pass
54
55    def create_non_pk_cols(self):
56        pass
57
58    def before_table(self):
59        pass
60
61    def create_tables(self):
62        pass
63
64    def after_table(self):
65        pass
66
67    def create_properties(self):
68        pass   
69
70    def before_mapper(self):
71        pass   
72
73    def after_mapper(self):
74        pass   
75
76    def finalize(self):
77        pass   
78
79
80class CounterMeta(type):
81    '''
82    A simple meta class which adds a ``_counter`` attribute to the instances of
83    the classes it is used on. This counter is simply incremented for each new
84    instance.
85    '''
86    counter = 0
87
88    def __call__(self, *args, **kwargs):
89        instance = type.__call__(self, *args, **kwargs)
90        instance._counter = CounterMeta.counter
91        CounterMeta.counter += 1
92        return instance
93
94
95class Property(EntityBuilder):
96    '''
97    Abstract base class for all properties of an Entity.
98    '''
99    __metaclass__ = CounterMeta
100   
101    def __init__(self, *args, **kwargs):
102        self.entity = None
103        self.name = None
104
105    def attach(self, entity, name):
106        """Attach this property to its entity, using 'name' as name.
107
108        Properties will be attached in the order they were declared.
109        """
110        self.entity = entity
111        self.name = name
112
113        # register this property as a builder
114        entity._descriptor.builders.append(self)
115
116        # delete the original attribute so that it doesn't interfere with
117        # SQLAlchemy.
118        if hasattr(entity, name):
119            delattr(entity, name)
120
121    def __repr__(self):
122        return "Property(%s, %s)" % (self.name, self.entity)
123
124
125class GenericProperty(Property):
126    '''
127    Generic catch-all class to wrap an SQLAlchemy property.
128
129    .. sourcecode:: python
130
131        class OrderLine(Entity):
132            quantity = Field(Float)
133            unit_price = Field(Numeric)
134            price = GenericProperty(lambda c: column_property(
135                             (c.quantity * c.unit_price).label('price')))
136    '''
137   
138    def __init__(self, prop):
139        super(GenericProperty, self).__init__()
140        self.prop = prop
141
142    def create_properties(self):
143        if callable(self.prop):
144            prop_value = self.prop(self.entity.table.c)
145        else:
146            prop_value = self.prop
147        prop_value = self.evaluate_property(prop_value)
148        self.entity._descriptor.add_property(self.name, prop_value)
149
150    def evaluate_property(self, prop):
151        return prop
152
153
154class ColumnProperty(GenericProperty):
155    '''
156    A specialized form of the GenericProperty to generate SQLAlchemy
157    ``column_property``'s.
158   
159    It takes a single argument, which is a function (often
160    given as an anonymous lambda) taking one argument and returning the
161    desired (scalar-returning) SQLAlchemy ClauseElement. That function will be
162    called whenever the entity table is completely defined, and will be given
163    the .c attribute of the entity as argument (as a way to access the entity
164    columns). The ColumnProperty will first wrap your ClauseElement in a label
165    with the same name as the property, then wrap that in a column_property.
166
167    .. sourcecode:: python
168
169        class OrderLine(Entity):
170            quantity = Field(Float)
171            unit_price = Field(Numeric)
172            price = ColumnProperty(lambda c: c.quantity * c.unit_price)
173
174    Please look at the `corresponding SQLAlchemy
175    documentation <http://www.sqlalchemy.org/docs/04/mappers.html
176    #advdatamapping_mapper_expressions>`_ for details.
177    '''
178
179    def evaluate_property(self, prop):
180        return column_property(prop.label(None))
181
182
183class Synonym(GenericProperty):
184    '''
185    This class represents a synonym property of another property (column, ...)
186    of an entity.  As opposed to the `synonym` kwarg to the Field class (which
187    share the same goal), this class can be used to define a synonym of a
188    property defined in a parent class (of the current class). On the other
189    hand, it cannot define a synonym for the purpose of using a standard python
190    property in queries. See the Field class for details on that usage.
191
192    .. sourcecode:: python
193
194    class Person(Entity):
195        name = Field(String(30))
196        primary_email = Field(String(100))
197        email_address = Synonym('primary_email')
198
199    class User(Person):
200        user_name = Synonym('name')
201        password = Field(String(20))
202    '''
203
204    def evaluate_property(self, prop):
205        return synonym(prop)
206
207#class Composite(GenericProperty):
208#    def __init__(self, prop):
209#        super(GenericProperty, self).__init__()
210#        self.prop = prop
211
212#    def evaluate_property(self, prop):
213#        return composite(prop.label(self.name))
214
215#start = Composite(Point, lambda c: (c.x1, c.y1))
216
217#mapper(Vertex, vertices, properties={
218#    'start':composite(Point, vertices.c.x1, vertices.c.y1),
219#    'end':composite(Point, vertices.c.x2, vertices.c.y2)
220#})
221
222
223has_property = PropertyStatement(GenericProperty)
Note: See TracBrowser for help on using the browser.