Package pyperry :: Module association
[frames] | no frames]

Source Code for Module pyperry.association

  1  import re 
  2  import pyperry.base 
  3  from pyperry import errors 
  4  from pyperry.relation import Relation 
  5   
6 -class Association(object):
7 """ 8 Associations 9 ============ 10 11 Associations allow you to retrieve a model or collection of models that are 12 related to one another in some way. Here we are concerned with how 13 associations are defined and implemented. For documentation on how to use 14 associations in your models, please see the L{belongs_to 15 <pyperry.Base.belongs_to>}, L{has_one <pyperry.Base.has_one>}, and 16 L{has_many <pyperry.Base.has_many>} methods on L{pyperry.Base}. 17 18 Terminology 19 ----------- 20 21 To understand how associations work, we must define the concepts of a 22 source class and a target class for an association. 23 24 - B{target class}: the class on which you define the association 25 - B{source class}: the class of the records returned by calling the 26 association method on the target class 27 28 An example showing the difference between source and target classes:: 29 30 class Car(pyperry.Base): pass 31 class Part(pyperry.Base): pass 32 33 # Car is the target class and Part is the source class 34 Car.has_many('parts', class_name='Part') 35 36 # Part is the target class and Car is the source class 37 Part.belongs_to('car', class_name='Car') 38 39 c = Car.first() 40 # returns a collection of Part models (Part is the source class) 41 c.parts() 42 43 p = Part.first() 44 # returns a Car model (Car is the source class) 45 p.car() 46 47 What happens when you define an association 48 ------------------------------------------- 49 50 Now let's look at what happens when you define an association on a model. 51 We will use the association C{Car.has_many('parts', class_name='Part')} as 52 an example because all associations work in the same general way. In this 53 example, a C{HasMany} class (an C{Association} subclass) is instantiated 54 where C{Car} is given as the C{target_klass} argument, and C{'parts'} is 55 given as the C{id} argument. Because we passed in C{'Part'} for the 56 C{class_name} option, it is used as the source class for this association. 57 58 The association id is used as the name of the association method that gets 59 defined on the target class. So in our example, all C{Car} instances now 60 have a C{parts()} method that can be called to retrieve a collection of 61 parts for a car. When you call the association method on the target class, 62 a relation (or scope) is constructed for the source class representing all 63 of the records related to the target class. For associations that represent 64 collections, such as C{has_many}, a relation is returned that you can 65 further modify. For associations that represent a single object, such as 66 C{belongs_to} or C{has_one}, an instance of that model is returned. 67 68 In summary, all an association does is create a scope on the source class 69 that represents records from the source class that are related to (or 70 associated with) the target class. Then a method on the target class is 71 created that returns this scope. 72 73 This means that calling C{car.parts()} is just returning a scope like:: 74 75 Part.scoped().where({'car_id': car.id}) 76 77 Similarly, calling C{part.car()} is just returning a scope like:: 78 79 Car.scoped().where({'id': part.car_id}).first() 80 81 """ 82
83 - def __init__(self, target_klass, id, **kwargs):
84 self.target_klass = target_klass 85 self.id = id 86 self.options = kwargs
87
88 - def __call__(self, obj):
89 if self.collection(): 90 return self.scope(obj) 91 return self.scope(obj).first()
92
93 - def type(self):
94 raise NotImplementedError, 'You must define the type in subclasses.'
95
96 - def polymorphic(self):
97 raise NotImplementedError, ('You must define how your association is' 98 'polymorphic in subclasses.')
99
100 - def collection(self):
101 raise NotImplementedError, 'You must define collection in subclasses.'
102
103 - def scope(self):
104 raise NotImplementedError, 'You must define scope in subclasses.'
105
106 - def primary_key(self, target_instance=None):
107 pk_option = self.options.get('primary_key') 108 if pk_option is not None: 109 primary_key = pk_option 110 elif isinstance(self, BelongsTo): 111 primary_key = self.source_klass(target_instance).primary_key() 112 else: 113 primary_key = self.target_klass.primary_key() 114 115 return primary_key
116 117 # Foreign key attributes
118 - def get_foreign_key(self):
119 return self.options.get('foreign_key')
120
121 - def set_foreign_key(self, value):
122 self.options['foreign_key'] = value
123 foreign_key = property(get_foreign_key, set_foreign_key) 124
125 - def eager_loadable(self):
126 for option in self.finder_options(): 127 if type(self.options.get(option)).__name__ == 'function': 128 return False 129 return not (self.type() == 'belongs_to' and self.polymorphic())
130 131 # MBM: This needs to be moved somewhere else. Probably in a constant.
132 - def finder_options(self):
135 136
137 - def source_klass(self, obj=None):
138 eager_loading = isinstance(obj, list) 139 poly_type = None 140 if (self.options.has_key('polymorphic') and 141 self.options['polymorphic'] and obj): 142 if type(obj.__class__) in [pyperry.base.Base, pyperry.base.BaseMeta]: 143 poly_type = getattr(obj, '%s_type' % self.id) 144 else: 145 poly_type = obj 146 147 if eager_loading and not self.eager_loadable(): 148 raise errors.AssociationPreloadNotSupported( 149 "This association cannot be eager loaded. It either has " 150 "a config with callables, or it is a polymorphic belongs " 151 "to association.") 152 153 if poly_type: 154 type_list = [ 155 self.options.get('namespace'), 156 self._sanitize_type_attribute(poly_type) 157 ] 158 type_string = '.'.join([arg for arg in type_list if arg]) 159 return self._get_resolved_class(type_string) 160 else: 161 if not self.options.get('klass') and not self.options.get('class_name'): 162 raise errors.ArgumentError, ('klass or class_name option' 163 ' required for association declaration.') 164 if self.options.get('klass'): 165 if type(self.options.get('klass')).__name__ == 'function': 166 return self.options.get('klass')() 167 return self.options.get('klass') 168 elif self.options.get('class_name'): 169 type_list = [ 170 self.options.get('namespace'), 171 self.options['class_name'] 172 ] 173 type_string = '.'.join([arg for arg in type_list if arg]) 174 return self._get_resolved_class(type_string)
175
176 - def _get_resolved_class(self, string):
177 class_name = self.target_klass.resolve_name(string) 178 if not class_name: 179 raise errors.ModelNotDefined, 'Model %s is not defined.' % (string) 180 elif len(class_name) > 1: 181 raise errors.AmbiguousClassName, ('Class name %s is' 182 ' ambiguous. Use the namespace option to get your' 183 ' specific class. Got classes %s' % (string, str(class_name))) 184 return class_name[0]
185
186 - def _base_scope(self, obj):
187 return self.source_klass(obj).scoped().apply_finder_options( 188 self._base_finder_options(obj))
189
190 - def _base_finder_options(self, obj):
191 opts = {} 192 for option in self.finder_options(): 193 value = self.options.get(option) 194 if value: 195 if type(self.options[option]).__name__ == 'function': 196 opts[option] = value() 197 else: 198 opts[option] = value 199 return opts
200
201 - def _sanitize_type_attribute(self, string):
202 return re.sub('[^a-zA-z]\w*', '', string)
203
204 -class BelongsTo(Association):
205 """ 206 Builds the association scope for a C{belongs} association. See the 207 L{Association} class for more details on how associations work. 208 209 """ 210
211 - def type(self):
212 return 'belongs_to'
213
214 - def collection(self):
215 return False
216 217 # Foreign key attributes
218 - def get_foreign_key(self):
219 return super(BelongsTo, self).foreign_key or '%s_id' % self.id
220
221 - def set_foreign_key(self, value):
222 self.options['foreign_key'] = value
223 foreign_key = property(get_foreign_key, set_foreign_key) 224
225 - def polymorphic(self):
226 return self.options.has_key('polymorphic') and self.options['polymorphic']
227
228 - def polymorphic_type(self):
229 return '%s_type' % self.id
230
231 - def scope(self, obj_or_list):
232 """ 233 Returns a scope on the source class containing this association 234 235 Builds conditions on top of the base_scope generated from any finder 236 options set with the association:: 237 238 belongs_to('foo', foreign_key='foo_id') 239 240 In addition to any finder options included with the association options 241 the following scope will be added:: 242 243 where('id = %s' % target['foo_id']) 244 245 """ 246 if isinstance(obj_or_list, pyperry.Base): 247 keys = obj_or_list[self.foreign_key] 248 else: 249 keys = [o[self.foreign_key] for o in obj_or_list] 250 251 if keys is not None: 252 return self._base_scope(obj_or_list).where({ 253 self.primary_key(obj_or_list): keys 254 })
255
256 -class Has(Association):
257 """ 258 Builds the association scope for a C{has} association. This is the 259 superclass for L{HasOne} and L{HasMany} associations. The only difference 260 between a has one and has many relation, is that C{.first()} is called on 261 the has one association's scope but not on the has many association's 262 scope. See the L{Association} class for more details on how associations 263 work. 264 265 """ 266 267 # Foreign key attributes
268 - def get_foreign_key(self):
269 if super(Has, self).foreign_key: 270 return super(Has, self).foreign_key 271 elif self.polymorphic(): 272 return '%s_id' % self.options['as_'] 273 else: 274 return '%s_id' % self.target_klass.__name__.lower()
275
276 - def set_foreign_key(self, value):
277 self.options['foreign_key'] = value
278 foreign_key = property(get_foreign_key, set_foreign_key) 279
280 - def polymorphic(self):
281 return self.options.has_key('as_')
282
283 - def polymorphic_type(self):
284 return '%s_type' % self.options['as_']
285
286 - def scope(self, obj_or_list):
287 """ 288 Returns a scope on the source class containing this association 289 290 Builds conditions on top of the base_scope generated from any finder 291 options set with the association:: 292 293 has_many('widgets', klass=Widget, foreign_key='widget_id') 294 has_many('comments', as_='parent') 295 296 In addition to any finder options included with the association options 297 the following will be added:: 298 299 where('widget_id = %s ' % target['id']) 300 301 Or for the polymorphic :comments association:: 302 303 where('parent_id = %s AND parent_type = %s' % (target['id'], 304 target.class)) 305 306 """ 307 pk_attr = self.primary_key() 308 if isinstance(obj_or_list, pyperry.Base): 309 keys = obj_or_list[pk_attr] 310 obj = obj_or_list 311 else: 312 keys = [o[pk_attr] for o in obj_or_list] 313 obj = obj_or_list[0] 314 315 if keys is not None: 316 scope = self._base_scope(obj_or_list).where({ 317 self.foreign_key: keys 318 }) 319 if self.polymorphic(): 320 scope = scope.where({ 321 self.polymorphic_type(): obj.__class__.__name__ 322 }) 323 return scope
324
325 -class HasMany(Has):
326 """ 327 The C{HasMany} class simply declares that the L{Has} association is a 328 collection of type C{'has_many'}. 329 330 """ 331
332 - def type(self):
333 return 'has_many'
334
335 - def collection(self):
336 return True
337
338 -class HasOne(Has):
339 """ 340 The C{HasOne} class simply declares that the L{Has} association is a 341 not a collection and has a type of C{'has_one'}. 342 343 """ 344
345 - def type(self):
346 return 'has_one'
347
348 - def collection(self):
349 return False
350
351 -class HasManyThrough(Has):
352 """ 353 The C{HasManyThrough} class is used whenever a C{has_many} association is 354 created with the C{through} option. It works by using the association id 355 given with the through option as a I{proxy} association to another class 356 on which a I{source} association is defined. The source class for a has 357 many through association will be the source class of the source 358 association. This means that the scope for a has many through association 359 is actually two scopes chained together. The proxy scope is used to 360 retrieve the records on which to build the source association, which is 361 used to build a scope for the records represented by the has many through 362 association. The proxy and source associations may be any of the simple has 363 or belongs to association types. 364 365 B{Options specific to has many through associations} 366 367 - B{through}: the association id of an association defined on the 368 target class. This association will be used as the proxy association, 369 and it is an error if this association does not exist. 370 371 - B{source}: the id of the source association. If the source option is 372 not specified, we assume that the source association's id is the same 373 as the has many through association's id. 374 375 - B{source_type}: the name of the source class as a string. This option 376 may be required if the source class is ambiguous, such as when the 377 source association is a polymorphic belongs_to association. 378 379 B{Has many through example} 380 381 This example shows how to create a basic has many through relationship in 382 which the internet has many connected devices through its networks. The has 383 many through association is defined on the Internet class. Notice how the 384 proxy association, C{networks}, is defined on the target class, 385 C{Internet}, and the source association, C{devices}, is defined on the 386 proxy association's source class, C{Network}. Therefore, the source class 387 for the entire relation is the source association's source class, 388 C{Device}:: 389 390 class Internet(pyperry.Base): 391 def _config(cls): 392 cls.attributes('id') 393 cls.has_many('connected_devices', through='networks', source='devices') 394 cls.has_many('networks', class_name='Network') 395 396 class Network(pyperry.Base): 397 def _config(cls): 398 cls.attributes('id', 'internet_id') 399 cls.belongs_to('internet', class_name='Internet') 400 cls.has_many('devices', class_name='Device') 401 402 class Device(pyperry.Base): 403 def _config(cls): 404 cls.attributes('id', 'network_id') 405 cls.belongs_to('network', class_name='Network') 406 407 """ 408
409 - def type(self):
410 return 'has_many_through'
411
412 - def collection(self):
413 return True
414
415 - def polymorphic(self):
416 return False
417
418 - def proxy_association(self):
419 if not hasattr(self, '_proxy_association'): 420 through = self.options.get('through') 421 proxy = self.target_klass.defined_associations.get(through) 422 if not proxy: raise errors.AssociationNotFound( 423 "has_many_through: '%s' is not an association on %s" % ( 424 str(through), str(self.target_klass))) 425 self._proxy_association = proxy 426 return self._proxy_association
427
428 - def source_association(self):
429 if not hasattr(self, '_source_association'): 430 source = self.proxy_association().source_klass() 431 source_option = self.options.get('source') 432 association = (source.defined_associations.get(self.id) or 433 source.defined_associations.get(source_option)) 434 if not association: raise errors.AssociationNotFound( 435 "has_many_through: '%s' is not an association on %s" % ( 436 str(source_option or self.id), str(source))) 437 self._source_association = association 438 return self._source_association
439
440 - def source_klass(self):
441 source_type = self.options.get('source_type') 442 return self.source_association().source_klass(source_type)
443
444 - def scope(self, obj):
445 source = self.source_association() 446 proxy = self.proxy_association() 447 key_attr = (source.foreign_key if source.type() == 'belongs_to' else 448 source.primary_key()) 449 450 proxy_ids = (lambda: [getattr(x, key_attr) for x in proxy.scope(obj)]) 451 452 relation = self.source_klass().scoped() 453 if source.type() == 'belongs_to': 454 source_type_option = self.options.get('source_type') 455 relation = relation.where(lambda: { 456 source.primary_key(source_type_option): proxy_ids() 457 }) 458 else: 459 relation = relation.where(lambda: { 460 source.foreign_key: proxy_ids() 461 }) 462 if source.polymorphic(): 463 proxy_source = proxy.source_klass(obj) 464 poly_type_attr = source.polymorphic_type() 465 poly_type_name = proxy_source.__name__ 466 relation = relation.where({ poly_type_attr: poly_type_name }) 467 468 return relation
469