Changeset 82
- Timestamp:
- 03/05/07 18:03:08 (6 years ago)
- Location:
- elixir/trunk
- Files:
-
- 8 modified
-
CHANGES (modified) (1 diff)
-
elixir/__init__.py (modified) (6 diffs)
-
elixir/entity.py (modified) (5 diffs)
-
elixir/fields.py (modified) (2 diffs)
-
elixir/options.py (modified) (2 diffs)
-
elixir/relationships.py (modified) (18 diffs)
-
elixir/statements.py (modified) (1 diff)
-
tests/test_autoload.py (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
elixir/trunk/CHANGES
r81 r82 1 1 0.2.1 2 - Added support for autoloading/reflecting databases with 3 has_and_belongs_to_many relationships. The tablename argument is now 4 optional, but still recommended, otherwise you'll have to use the same exact 5 name for your intermediary table than the one generated. 6 - Made the colname argument optional for belongs_to relationships in 7 autoloaded entities. It is only required to specify it if you have several 8 belongs_to relationships between two entities/tables. 9 - Fixed bug preventing having entities without any statement. 2 10 - Applied patch from "Wavy" so that columns of a table are in the same order 3 11 as they were declared. 12 - Made some PEP8 tweaks in many places. Used the pep8 script provided with 13 Cheesecake. 14 - Foreign key names generated by belongs_to relationships use column names 15 instead of relation names in case we have a relation with the same name 16 defined in several entities inheriting from the same entity using single- 17 table inheritance (and we set a custom column name in one of them to avoid 18 a column-name conflict). 19 - Actually make the code python 2.3 compatible (Robin's patch was based on 20 0.1.0 while I had introduced more decorators in the trunk in the mean time). 21 - Some cleanup/useless code removal 4 22 5 23 0.2.0 - 2007-02-28 -
elixir/trunk/elixir/__init__.py
r73 r82 1 1 ''' 2 2 Elixir package 3 3 4 4 A declarative layer on top of SQLAlchemy, which is intended to replace the 5 5 ActiveMapper SQLAlchemy extension, and the TurboEntity project. Elixir is a 6 fairly thin wrapper around SQLAlchemy, which provides the ability to define 7 model objects following the Active Record design pattern, and using a DSL 6 fairly thin wrapper around SQLAlchemy, which provides the ability to define 7 model objects following the Active Record design pattern, and using a DSL 8 8 syntax similar to that of the Ruby on Rails ActiveRecord system. 9 9 10 Elixir does not intend to replace SQLAlchemy's core features, but instead 10 Elixir does not intend to replace SQLAlchemy's core features, but instead 11 11 focuses on providing a simpler syntax for defining model objects when you do 12 12 not need the full expressiveness of SQLAlchemy's manual mapper definitions. … … 31 31 from sets import Set as set 32 32 33 __all__ = ['Entity', 'Field', 'has_field', 'with_fields', 34 'belongs_to', 'has_one', 'has_many', 'has_and_belongs_to_many', 33 __all__ = ['Entity', 'Field', 'has_field', 'with_fields', 34 'belongs_to', 'has_one', 'has_many', 'has_and_belongs_to_many', 35 35 'using_options', 'using_table_options', 'using_mapper_options', 36 36 'options_defaults', 'metadata', 'objectstore', 37 'create_all', 'drop_all', 'setup_all', 'cleanup_all', 37 'create_all', 'drop_all', 'setup_all', 'cleanup_all', 38 38 'delay_setup'] + \ 39 39 sqlalchemy.types.__all__ … … 51 51 # thread local SessionContext 52 52 class Objectstore(object): 53 53 54 def __init__(self, *args, **kwargs): 54 55 self.context = SessionContext(*args, **kwargs) 56 55 57 def __getattr__(self, name): 56 58 return getattr(self.context.current, name) 57 59 session = property(lambda s:s.context.current) 58 60 59 61 objectstore = Objectstore(sqlalchemy.create_session) 60 62 61 63 metadatas = set() 64 62 65 63 66 def create_all(): … … 65 68 for md in metadatas: 66 69 md.create_all() 70 67 71 68 72 def drop_all(): … … 74 78 delay_setup = False 75 79 80 76 81 def setup_all(): 77 82 '''Setup the table and mapper for all entities which have been delayed. 78 83 79 84 This should be used in conjunction with setting ``delay_setup`` to ``True`` 80 85 before defining your entities. … … 95 100 create_all() 96 101 102 97 103 def cleanup_all(): 98 104 '''Drop table and clear mapper for all entities, and clear all metadatas. -
elixir/trunk/elixir/entity.py
r81 r82 161 161 assign_mapper(session.context, self.entity, self.entity.table, 162 162 properties=properties, **kwargs) 163 164 163 165 164 def setup_table(self): … … 259 258 "inverse relations manually by using the inverse " 260 259 "keyword." 261 % (rel.name, rel.entity.__name__) 262 ) 260 % (rel.name, rel.entity.__name__)) 263 261 # When a matching inverse is found, we check that it has only 264 262 # one relation matching as its own inverse. We don't need the result … … 270 268 return matching_rel 271 269 272 @property273 270 def all_relationships(self): 274 271 if self.parent: … … 278 275 res.update(self.relationships) 279 276 return res 277 all_relationships = property(all_relationships) 280 278 281 279 def setup_relationships(cls): … … 310 308 311 309 class __metaclass__(type): 310 312 311 def __init__(cls, name, bases, dict_): 313 312 # only process subclasses of Entity, not Entity itself -
elixir/trunk/elixir/fields.py
r74 r82 115 115 116 116 class HasField(object): 117 117 118 def __init__(self, entity, name, *args, **kwargs): 118 119 field = Field(*args, **kwargs) … … 122 123 123 124 class WithFields(object): 125 124 126 def __init__(self, entity, *args, **fields): 125 127 columns = list() -
elixir/trunk/elixir/options.py
r71 r82 135 135 136 136 137 class UsingTableOptions(object): 137 class UsingTableOptions(object): 138 138 139 def __init__(self, entity, *args, **kwargs): 139 140 entity._descriptor.table_args = list(args) … … 142 143 143 144 class UsingMapperOptions(object): 145 144 146 def __init__(self, entity, *args, **kwargs): 145 147 entity._descriptor.mapper_options.update(kwargs) -
elixir/trunk/elixir/relationships.py
r74 r82 180 180 181 181 def __init__(self, entity, name, *args, **kwargs): 182 self.entity = entity 182 183 self.name = name 183 184 self.of_kind = kwargs.pop('of_kind') 184 185 self.inverse_name = kwargs.pop('inverse', None) 185 186 186 self.entity = entity187 187 self._target = None 188 189 188 self._inverse = None 190 self.foreign_key = kwargs.pop('foreign_key', None)191 if self.foreign_key and not isinstance(self.foreign_key, list):192 self.foreign_key = [self.foreign_key]193 189 194 190 self.property = None # sqlalchemy property … … 276 272 "Couldn't find a relationship named '%s' in " 277 273 "entity '%s' or its parent entities." 278 % (self.inverse_name, self.target.__name__) 279 ) 274 % (self.inverse_name, self.target.__name__)) 280 275 assert self.match_type_of(inverse) 281 276 else: … … 316 311 317 312 def __init__(self, entity, name, *args, **kwargs): 318 self.colname = kwargs.pop('colname', None) 313 self.colname = kwargs.pop('colname', []) 314 if self.colname and not isinstance(self.colname, list): 315 self.colname = [self.colname] 316 319 317 self.column_kwargs = kwargs.pop('column_kwargs', {}) 320 318 if 'required' in kwargs: … … 324 322 if 'use_alter' in kwargs: 325 323 self.contraint_kwargs['use_alter'] = kwargs.pop('use_alter') 326 327 if self.colname and not isinstance(self.colname, list):328 self.colname = [self.colname]324 325 self.foreign_key = list() 326 self.primaryjoin_clauses = list() 329 327 super(BelongsTo, self).__init__(entity, name, *args, **kwargs) 330 328 … … 334 332 source accordingly. 335 333 ''' 336 334 337 335 source_desc = self.entity._descriptor 338 336 target_desc = self.target._descriptor 339 340 # convert strings to column instances341 if self.foreign_key:342 #FIXME: this will fail. Because if we specify a foreign_key343 # as argument, it will not create the necessary column344 self.foreign_key = [source_desc.fields[k].column345 for k in self.foreign_key346 if isinstance(k, basestring)]347 return348 349 self.foreign_key = list()350 self.primaryjoin_clauses = list()351 337 352 338 if source_desc.autoload: 353 if not self.colname:354 raise Exception(355 "Entity '%s' is autoloaded but relation '%s' has no "356 "column name specified. You should specify it by "357 "using the colname keyword."358 % (self.entity.__name__, self.name)359 )360 339 361 340 #TODO: test if this works when colname is a list 362 341 for colname in self.colname: 342 col_found = False 363 343 for col in self.entity.table.columns: 364 344 if col.name == colname: … … 369 349 fk = fk_iter.next() 370 350 self.primaryjoin_clauses.append(col == fk.column) 371 372 if not self.primaryjoin_clauses: 373 raise Exception("Column '%s' not found in table '%s'" 374 % (self.colname, self.entity.table.name)) 351 col_found = True 352 353 if not col_found: 354 raise Exception("Column '%s' not found in table '%s'" 355 % (colname, self.entity.table.name)) 375 356 else: 376 357 fk_refcols = list() … … 385 366 "of the primary key of '%s'." 386 367 % (self.name, self.entity.__name__, 387 self.target.__name__) 388 ) 368 self.target.__name__)) 389 369 390 370 for key_num, key in enumerate(target_desc.primary_keys): … … 402 382 source_desc.add_field(field) 403 383 404 self.foreign_key.append(field.column)405 406 384 # build the list of local columns which will be part of 407 385 # the foreign key 386 self.foreign_key.append(field.column) 387 388 # store the names of those columns 408 389 fk_colnames.append(colname) 409 390 … … 418 399 # In some databases (at lease MySQL) the constraint name needs to 419 400 # be unique for the whole database, instead of per table. 420 fk_name = "%s_%s_fk" % (self.entity.table.name, self.name) 401 fk_name = "%s_%s_fk" % (self.entity.table.name, 402 '_'.join(fk_colnames)) 421 403 source_desc.add_constraint(ForeignKeyConstraint( 422 404 fk_colnames, fk_refcols, … … 434 416 kwargs['remote_side'] = cols 435 417 436 kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses) 418 if self.primaryjoin_clauses: 419 kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses) 437 420 kwargs['uselist'] = False 438 421 … … 445 428 446 429 def create_keys(self): 447 # make sure the inverseexists430 # make sure an inverse relationship exists 448 431 if self.inverse is None: 449 432 raise Exception( … … 455 438 "manually by using the inverse keyword." 456 439 % (self.target.__name__, self.name, 457 self.entity.__name__) 458 ) 440 self.entity.__name__)) 459 441 # make sure it is set up because it creates the foreign key we'll need 460 442 self.inverse.setup() … … 469 451 # already setup in the other way (belongs_to). 470 452 if self.entity.table is self.target.table: 453 #FIXME: IF this code is of any use, it will probably break for 454 # autoloaded tables 471 455 kwargs['remote_side'] = self.inverse.foreign_key 472 456 473 kwargs['primaryjoin'] = and_(*self.inverse.primaryjoin_clauses) 457 if self.inverse.primaryjoin_clauses: 458 kwargs['primaryjoin'] = and_(*self.inverse.primaryjoin_clauses) 459 474 460 kwargs['uselist'] = self.uselist 475 461 … … 491 477 492 478 class HasAndBelongsToMany(Relationship): 479 493 480 def __init__(self, entity, name, *args, **kwargs): 494 481 self.user_tablename = kwargs.pop('tablename', None) 495 482 self.secondary_table = None 483 self.primaryjoin_clauses = list() 484 self.secondaryjoin_clauses = list() 496 485 super(HasAndBelongsToMany, self).__init__(entity, name, 497 486 *args, **kwargs) … … 507 496 e1_desc = self.entity._descriptor 508 497 e2_desc = self.target._descriptor 498 499 # First, we compute the name of the table. Note that some of the 500 # intermediary variables are reused later for the constraint 501 # names. 509 502 510 if e1_desc.autoload:511 if not self.user_tablename:512 raise Exception(513 "Entity '%s' is autoloaded but relation '%s' has no "514 "secondary table name specified. You should specify "515 "it by using the tablename keyword."516 % (self.entity.__name__, self.name)517 )518 519 503 # We use the name of the relation for the first entity 520 504 # (instead of the name of its primary key), so that we can … … 523 507 source_part = "%s_%s" % (e1_desc.tablename, self.name) 524 508 525 # And we use the name of the primary key forthe second entity509 # And we use only the name of the table of the second entity 526 510 # when there is no inverse, so that a many-to-many relation 527 511 # can be defined without an inverse. 528 512 if self.inverse: 529 e2_name = self.inverse.name513 target_part = "%s_%s" % (e2_desc.tablename, self.inverse.name) 530 514 else: 531 e2_name = '_'.join([key.column.name for key in 532 e2_desc.primary_keys]) 533 target_part = "%s_%s" % (e2_desc.tablename, e2_name) 515 target_part = e2_desc.tablename 534 516 535 517 if self.user_tablename: 536 518 tablename = self.user_tablename 537 519 else: 538 # we need to keep the table name consistent (independant of539 # whether this relation or its inverse is setup first) 520 # We need to keep the table name consistent (independant of 521 # whether this relation or its inverse is setup first). 540 522 if self.inverse and e1_desc.tablename < e2_desc.tablename: 541 523 tablename = "%s__%s" % (target_part, source_part) … … 543 525 tablename = "%s__%s" % (source_part, target_part) 544 526 545 # In some databases (at lease MySQL) the constraint names need 546 # to be unique for the whole database, instead of per table. 547 source_fk_name = "%s_fk" % source_part 548 if self.inverse: 549 target_fk_name = "%s_fk" % target_part 527 if e1_desc.autoload: 528 self._reflect_table(tablename) 550 529 else: 551 target_fk_name = "%s_inverse_fk" % source_part 552 553 columns = list() 554 constraints = list() 555 556 self.primaryjoin_clauses = list() 557 self.secondaryjoin_clauses = list() 558 559 for num, desc, join_name, fk_name in ( 560 ('1', e1_desc, 'primary', source_fk_name), 561 ('2', e2_desc, 'secondary', target_fk_name)): 562 fk_colnames = list() 563 fk_refcols = list() 564 565 for key in desc.primary_keys: 566 pk_col = key.column 530 # We pre-compute the names of the foreign key constraints 531 # pointing to the source (local) entity's table and to the 532 # target's table 533 534 # In some databases (at lease MySQL) the constraint names need 535 # to be unique for the whole database, instead of per table. 536 source_fk_name = "%s_fk" % source_part 537 if self.inverse: 538 target_fk_name = "%s_fk" % target_part 539 else: 540 target_fk_name = "%s_inverse_fk" % source_part 541 542 columns = list() 543 constraints = list() 544 545 joins = (self.primaryjoin_clauses, self.secondaryjoin_clauses) 546 for num, desc, fk_name in ((0, e1_desc, source_fk_name), 547 (1, e2_desc, target_fk_name)): 548 fk_colnames = list() 549 fk_refcols = list() 550 551 for key in desc.primary_keys: 552 pk_col = key.column 553 554 colname = '%s_%s' % (desc.tablename, pk_col.name) 555 556 # In case we have a many-to-many self-reference, we 557 # need to tweak the names of the columns so that we 558 # don't end up with twice the same column name. 559 if self.entity is self.target: 560 colname += str(num + 1) 561 562 col = Column(colname, pk_col.type) 563 columns.append(col) 564 565 # Build the list of local columns which will be part 566 # of the foreign key. 567 fk_colnames.append(colname) 568 569 # Build the list of columns the foreign key will point 570 # to. 571 fk_refcols.append(desc.tablename + '.' + pk_col.name) 572 573 # Build join clauses (in case we have a self-ref) 574 if self.entity is self.target: 575 joins[num].append(col == pk_col) 567 576 568 colname = '%s_%s' % (desc.tablename, pk_col.name) 569 570 # In case we have a many-to-many self-reference, we need 571 # to tweak the names of the columns so that we don't end 572 # up with twice the same column name. 573 if self.entity is self.target: 574 colname += num 575 576 col = Column(colname, pk_col.type) 577 columns.append(col) 578 579 # build the list of local columns which will be part of 580 # the foreign key 581 fk_colnames.append(colname) 582 583 # build the list of columns the foreign key will point to 584 fk_refcols.append(desc.tablename + '.' + pk_col.name) 585 586 # build join clauses 587 join_list = getattr(self, join_name+'join_clauses') 588 join_list.append(col == pk_col) 577 constraints.append( 578 ForeignKeyConstraint(fk_colnames, fk_refcols, 579 name=fk_name)) 580 581 args = columns + constraints 589 582 590 constraints.append( 591 ForeignKeyConstraint(fk_colnames, fk_refcols, 592 name=fk_name)) 593 594 595 args = columns + constraints 596 597 self.secondary_table = Table(tablename, e1_desc.metadata, *args) 598 583 self.secondary_table = Table(tablename, e1_desc.metadata, 584 *args) 585 586 def _reflect_table(self, tablename): 587 if not self.target._descriptor.autoload: 588 raise Exception( 589 "Entity '%s' is autoloaded and its '%s' " 590 "has_and_belongs_to_many relationship points to " 591 "the '%s' entity which is not autoloaded" 592 % (self.entity.__name__, self.name, 593 self.target.__name__)) 594 595 self.secondary_table = Table(tablename, 596 self.entity._descriptor.metadata, 597 autoload=True) 598 599 # In the case we have a self-reference, we need to build join clauses 600 if self.entity is self.target: 601 joins = (self.primaryjoin_clauses, 602 self.secondaryjoin_clauses) 603 join_nr = 0 604 605 # We loop through all the table constraints. The first 606 # ForeignKeyConstraint we find which points to the entity table 607 # will be used to build the primary join and the second one for 608 # the secondary join. 609 for constraint in self.secondary_table.constraints: 610 if isinstance(constraint, ForeignKeyConstraint): 611 use_constraint = False 612 for fk in constraint.elements: 613 if fk.references(self.entity.table): 614 use_constraint = True 615 if use_constraint: 616 for fk in constraint.elements: 617 joins[join_nr].append(fk.parent == 618 fk.column) 619 join_nr += 1 620 599 621 def create_properties(self): 600 622 kwargs = self.kwargs … … 618 640 619 641 620 belongs_to = Statement(BelongsTo)621 has_one = Statement(HasOne)622 has_many = Statement(HasMany)642 belongs_to = Statement(BelongsTo) 643 has_one = Statement(HasOne) 644 has_many = Statement(HasMany) 623 645 has_and_belongs_to_many = Statement(HasAndBelongsToMany) -
elixir/trunk/elixir/statements.py
r70 r82 30 30 # loop over all statements in the class's statement list 31 31 # and apply them, i.e. instanciate the corresponding classes 32 for statement, args, kwargs in getattr(entity, STATEMENTS ):32 for statement, args, kwargs in getattr(entity, STATEMENTS, []): 33 33 statement.target(entity, *args, **kwargs) 34 34 process = classmethod(process) -
elixir/trunk/tests/test_autoload.py
r54 r82 11 11 import datetime 12 12 13 # First create t wo tables (it would be better to useran external db)13 # First create the tables (it would be better to use an external db) 14 14 engine = sqlalchemy.create_engine('sqlite:///') 15 15 meta = BoundMetaData(engine) … … 29 29 animal_table.create() 30 30 31 category_table = Table('category', meta, 32 Column('name', String, primary_key=True)) 33 category_table.create() 34 35 person_category_table = Table('person_category', meta, 36 Column('person_id', Integer, ForeignKey('person.id')), 37 Column('category_name', String, ForeignKey('category.name'))) 38 person_category_table.create() 39 40 person_person_table = Table('person_person', meta, 41 Column('person_id1', Integer, ForeignKey('person.id')), 42 Column('person_id2', Integer, ForeignKey('person.id'))) 43 person_person_table.create() 44 31 45 elixir.delay_setup = True 46 elixir.options_defaults.update(dict(autoload=True, shortnames=True)) 32 47 33 48 class Person(Entity): 34 # has_field('name', Unicode(32))35 36 49 belongs_to('father', of_kind='Person', colname='father_id') 37 50 has_many('children', of_kind='Person') 38 51 has_many('pets', of_kind='Animal', inverse='owner') 39 52 has_many('animals', of_kind='Animal', inverse='feeder') 40 41 using_options(autoload=True, shortnames=True) 53 has_and_belongs_to_many('categories', of_kind='Category', 54 tablename='person_category') 55 has_and_belongs_to_many('friends', of_kind='Person', 56 tablename='person_person') 42 57 43 58 def __str__(self): … … 47 62 return s 48 63 64 49 65 class Animal(Entity): 50 # has_field('name', String(15))51 # has_field('color', String(15))52 53 66 belongs_to('owner', of_kind='Person', colname='owner_id') 54 67 belongs_to('feeder', of_kind='Person', colname='feeder_id') 55 68 56 using_options(autoload=True, shortnames=True) 69 70 class Category(Entity): 71 has_and_belongs_to_many('persons', of_kind='Person', 72 tablename='person_category') 57 73 58 74 elixir.delay_setup = False 75 elixir.options_defaults.update(dict(autoload=False, shortnames=False)) 59 76 60 77 #----------- … … 92 109 lisa = Person(name="Lisa") 93 110 94 grampa.children.append(homer) 111 grampa.children.append(homer) 95 112 homer.children.append(bart) 96 113 lisa.father = homer … … 109 126 assert p is Person.get_by(name="Lisa").father 110 127 128 def test_autoload_has_and_belongs_to_many(self): 129 stupid = Category(name="Stupid") 130 simpson = Category(name="Simpson") 131 old = Category(name="Old") 132 133 grampa = Person(name="Abe", categories=[simpson, old]) 134 homer = Person(name="Homer", categories=[simpson, stupid]) 135 bart = Person(name="Bart") 136 lisa = Person(name="Lisa") 137 138 simpson.persons.extend([bart, lisa]) 139 140 objectstore.flush() 141 objectstore.clear() 142 143 c = Category.get_by(name="Simpson") 144 grampa = Person.get_by(name="Abe") 145 146 print "Persons in the '%s' category: %s." % ( 147 c.name, 148 ", ".join(p.name for p in c.persons)) 149 150 assert len(c.persons) == 4 151 assert c in grampa.categories 152 153 def test_autoload_has_and_belongs_to_many_selfref(self): 154 barney = Person(name="Barney") 155 homer = Person(name="Homer", friends=[barney]) 156 barney.friends.append(homer) 157 158 objectstore.flush() 159 objectstore.clear() 160 161 homer = Person.get_by(name="Homer") 162 barney = Person.get_by(name="Barney") 163 164 assert homer in barney.friends 165 assert barney in homer.friends 166 111 167 if __name__ == '__main__': 112 168 test = TestAutoload()
