Changeset 82

Show
Ignore:
Timestamp:
03/05/07 18:03:08 (6 years ago)
Author:
ged
Message:

- Added support for autoloading/reflecting databases with

has_and_belongs_to_many relationships. The tablename argument is now
optional, but still recommended, otherwise you'll have to use the same exact
name for your intermediary table than the one generated.

- Made the colname argument optional for belongs_to relationships in

autoloaded entities. It is only required to specify it if you have several
belongs_to relationships between two entities/tables.

- Fixed bug preventing having entities without any statement.
- Made some PEP8 tweaks in many places. Used the pep8 script provided with

Cheesecake.

- Foreign key names generated by belongs_to relationships use column names

instead of relation names in case we have a relation with the same name
defined in several entities inheriting from the same entity using single-
table inheritance (and we set a custom column name in one of them to avoid
a column-name conflict).

- Actually make the code python 2.3 compatible (Robin's patch was based on

0.1.0 while I had introduced more decorators in the trunk in the mean time).

- Some cleanup/useless code removal (namely the foreign_key stuff in

relationships).

Location:
elixir/trunk
Files:
8 modified

Legend:

Unmodified
Added
Removed
  • elixir/trunk/CHANGES

    r81 r82  
    110.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. 
    210- Applied patch from "Wavy" so that columns of a table are in the same order 
    311  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 
    422 
    5230.2.0 - 2007-02-28 
  • elixir/trunk/elixir/__init__.py

    r73 r82  
    11''' 
    22Elixir package 
    3      
     3 
    44A declarative layer on top of SQLAlchemy, which is intended to replace the 
    55ActiveMapper 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  
     6fairly thin wrapper around SQLAlchemy, which provides the ability to define 
     7model objects following the Active Record design pattern, and using a DSL 
    88syntax similar to that of the Ruby on Rails ActiveRecord system. 
    99 
    10 Elixir does not intend to replace SQLAlchemy's core features, but instead  
     10Elixir does not intend to replace SQLAlchemy's core features, but instead 
    1111focuses on providing a simpler syntax for defining model objects when you do 
    1212not need the full expressiveness of SQLAlchemy's manual mapper definitions. 
     
    3131    from sets import Set as set 
    3232 
    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', 
    3535           'using_options', 'using_table_options', 'using_mapper_options', 
    3636           'options_defaults', 'metadata', 'objectstore', 
    37            'create_all', 'drop_all', 'setup_all', 'cleanup_all',  
     37           'create_all', 'drop_all', 'setup_all', 'cleanup_all', 
    3838           'delay_setup'] + \ 
    3939          sqlalchemy.types.__all__ 
     
    5151    # thread local SessionContext 
    5252    class Objectstore(object): 
     53 
    5354        def __init__(self, *args, **kwargs): 
    5455            self.context = SessionContext(*args, **kwargs) 
     56 
    5557        def __getattr__(self, name): 
    5658            return getattr(self.context.current, name) 
    5759        session = property(lambda s:s.context.current) 
    58      
     60 
    5961    objectstore = Objectstore(sqlalchemy.create_session) 
    6062 
    6163metadatas = set() 
     64 
    6265 
    6366def create_all(): 
     
    6568    for md in metadatas: 
    6669        md.create_all() 
     70 
    6771 
    6872def drop_all(): 
     
    7478delay_setup = False 
    7579 
     80 
    7681def setup_all(): 
    7782    '''Setup the table and mapper for all entities which have been delayed. 
    78      
     83 
    7984    This should be used in conjunction with setting ``delay_setup`` to ``True`` 
    8085    before defining your entities. 
     
    95100    create_all() 
    96101 
     102 
    97103def cleanup_all(): 
    98104    '''Drop table and clear mapper for all entities, and clear all metadatas. 
  • elixir/trunk/elixir/entity.py

    r81 r82  
    161161        assign_mapper(session.context, self.entity, self.entity.table, 
    162162                      properties=properties, **kwargs) 
    163  
    164163 
    165164    def setup_table(self): 
     
    259258                            "inverse relations manually by using the inverse " 
    260259                            "keyword." 
    261                             % (rel.name, rel.entity.__name__)  
    262                           ) 
     260                            % (rel.name, rel.entity.__name__)) 
    263261        # When a matching inverse is found, we check that it has only 
    264262        # one relation matching as its own inverse. We don't need the result 
     
    270268        return matching_rel 
    271269 
    272     @property 
    273270    def all_relationships(self): 
    274271        if self.parent: 
     
    278275        res.update(self.relationships) 
    279276        return res 
     277    all_relationships = property(all_relationships) 
    280278 
    281279    def setup_relationships(cls): 
     
    310308     
    311309    class __metaclass__(type): 
     310 
    312311        def __init__(cls, name, bases, dict_): 
    313312            # only process subclasses of Entity, not Entity itself 
  • elixir/trunk/elixir/fields.py

    r74 r82  
    115115 
    116116class HasField(object): 
     117 
    117118    def __init__(self, entity, name, *args, **kwargs): 
    118119        field = Field(*args, **kwargs) 
     
    122123 
    123124class WithFields(object): 
     125 
    124126    def __init__(self, entity, *args, **fields): 
    125127        columns = list() 
  • elixir/trunk/elixir/options.py

    r71 r82  
    135135 
    136136 
    137 class UsingTableOptions(object):     
     137class UsingTableOptions(object): 
     138 
    138139    def __init__(self, entity, *args, **kwargs): 
    139140        entity._descriptor.table_args = list(args) 
     
    142143 
    143144class UsingMapperOptions(object): 
     145 
    144146    def __init__(self, entity, *args, **kwargs): 
    145147        entity._descriptor.mapper_options.update(kwargs) 
  • elixir/trunk/elixir/relationships.py

    r74 r82  
    180180     
    181181    def __init__(self, entity, name, *args, **kwargs): 
     182        self.entity = entity 
    182183        self.name = name 
    183184        self.of_kind = kwargs.pop('of_kind') 
    184185        self.inverse_name = kwargs.pop('inverse', None) 
    185186         
    186         self.entity = entity 
    187187        self._target = None 
    188          
    189188        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] 
    193189         
    194190        self.property = None # sqlalchemy property 
     
    276272                              "Couldn't find a relationship named '%s' in " 
    277273                              "entity '%s' or its parent entities."  
    278                               % (self.inverse_name, self.target.__name__) 
    279                           ) 
     274                              % (self.inverse_name, self.target.__name__)) 
    280275                assert self.match_type_of(inverse) 
    281276            else: 
     
    316311     
    317312    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 
    319317        self.column_kwargs = kwargs.pop('column_kwargs', {}) 
    320318        if 'required' in kwargs: 
     
    324322        if 'use_alter' in kwargs: 
    325323            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() 
    329327        super(BelongsTo, self).__init__(entity, name, *args, **kwargs) 
    330328     
     
    334332        source accordingly. 
    335333        ''' 
    336          
     334 
    337335        source_desc = self.entity._descriptor 
    338336        target_desc = self.target._descriptor 
    339          
    340         # convert strings to column instances 
    341         if self.foreign_key: 
    342             #FIXME: this will fail. Because if we specify a foreign_key 
    343             # as argument, it will not create the necessary column 
    344             self.foreign_key = [source_desc.fields[k].column 
    345                                    for k in self.foreign_key  
    346                                        if isinstance(k, basestring)] 
    347             return 
    348  
    349         self.foreign_key = list() 
    350         self.primaryjoin_clauses = list() 
    351337 
    352338        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                       ) 
    360339 
    361340            #TODO: test if this works when colname is a list 
    362341            for colname in self.colname: 
     342                col_found = False 
    363343                for col in self.entity.table.columns: 
    364344                    if col.name == colname: 
     
    369349                        fk = fk_iter.next() 
    370350                        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)) 
    375356        else: 
    376357            fk_refcols = list() 
     
    385366                        "of the primary key of '%s'." 
    386367                        % (self.name, self.entity.__name__,  
    387                            self.target.__name__) 
    388                       ) 
     368                           self.target.__name__)) 
    389369 
    390370            for key_num, key in enumerate(target_desc.primary_keys): 
     
    402382                source_desc.add_field(field) 
    403383 
    404                 self.foreign_key.append(field.column) 
    405  
    406384                # build the list of local columns which will be part of 
    407385                # the foreign key 
     386                self.foreign_key.append(field.column) 
     387 
     388                # store the names of those columns 
    408389                fk_colnames.append(colname) 
    409390 
     
    418399            # In some databases (at lease MySQL) the constraint name needs to  
    419400            # 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)) 
    421403            source_desc.add_constraint(ForeignKeyConstraint( 
    422404                                            fk_colnames, fk_refcols, 
     
    434416            kwargs['remote_side'] = cols 
    435417 
    436         kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses) 
     418        if self.primaryjoin_clauses: 
     419            kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses) 
    437420        kwargs['uselist'] = False 
    438421         
     
    445428 
    446429    def create_keys(self): 
    447         # make sure the inverse exists 
     430        # make sure an inverse relationship exists 
    448431        if self.inverse is None: 
    449432            raise Exception( 
     
    455438                      "manually by using the inverse keyword." 
    456439                      % (self.target.__name__, self.name, 
    457                          self.entity.__name__) 
    458                   ) 
     440                         self.entity.__name__)) 
    459441        # make sure it is set up because it creates the foreign key we'll need 
    460442        self.inverse.setup() 
     
    469451        # already setup in the other way (belongs_to). 
    470452        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 
    471455            kwargs['remote_side'] = self.inverse.foreign_key 
    472456         
    473         kwargs['primaryjoin'] = and_(*self.inverse.primaryjoin_clauses) 
     457        if self.inverse.primaryjoin_clauses: 
     458            kwargs['primaryjoin'] = and_(*self.inverse.primaryjoin_clauses) 
     459 
    474460        kwargs['uselist'] = self.uselist 
    475461         
     
    491477 
    492478class HasAndBelongsToMany(Relationship): 
     479 
    493480    def __init__(self, entity, name, *args, **kwargs): 
    494481        self.user_tablename = kwargs.pop('tablename', None) 
    495482        self.secondary_table = None 
     483        self.primaryjoin_clauses = list() 
     484        self.secondaryjoin_clauses = list() 
    496485        super(HasAndBelongsToMany, self).__init__(entity, name,  
    497486                                                  *args, **kwargs) 
     
    507496            e1_desc = self.entity._descriptor 
    508497            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. 
    509502             
    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  
    519503            # We use the name of the relation for the first entity  
    520504            # (instead of the name of its primary key), so that we can  
     
    523507            source_part = "%s_%s" % (e1_desc.tablename, self.name) 
    524508 
    525             # And we use the name of the primary key for the second entity 
     509            # And we use only the name of the table of the second entity 
    526510            # when there is no inverse, so that a many-to-many relation  
    527511            # can be defined without an inverse. 
    528512            if self.inverse: 
    529                 e2_name = self.inverse.name 
     513                target_part = "%s_%s" % (e2_desc.tablename, self.inverse.name) 
    530514            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 
    534516             
    535517            if self.user_tablename: 
    536518                tablename = self.user_tablename 
    537519            else: 
    538                 # we need to keep the table name consistent (independant of  
    539                 # 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). 
    540522                if self.inverse and e1_desc.tablename < e2_desc.tablename: 
    541523                    tablename = "%s__%s" % (target_part, source_part) 
     
    543525                    tablename = "%s__%s" % (source_part, target_part) 
    544526 
    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) 
    550529            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) 
    567576                     
    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 
    589582                 
    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 
    599621    def create_properties(self): 
    600622        kwargs = self.kwargs 
     
    618640 
    619641 
    620 belongs_to              = Statement(BelongsTo) 
    621 has_one                 = Statement(HasOne) 
    622 has_many                = Statement(HasMany) 
     642belongs_to = Statement(BelongsTo) 
     643has_one = Statement(HasOne) 
     644has_many = Statement(HasMany) 
    623645has_and_belongs_to_many = Statement(HasAndBelongsToMany) 
  • elixir/trunk/elixir/statements.py

    r70 r82  
    3030        # loop over all statements in the class's statement list  
    3131        # 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, []): 
    3333            statement.target(entity, *args, **kwargs) 
    3434    process = classmethod(process) 
  • elixir/trunk/tests/test_autoload.py

    r54 r82  
    1111import datetime 
    1212 
    13 # First create two tables (it would be better to user an external db) 
     13# First create the tables (it would be better to use an external db) 
    1414engine = sqlalchemy.create_engine('sqlite:///') 
    1515meta = BoundMetaData(engine) 
     
    2929animal_table.create() 
    3030 
     31category_table = Table('category', meta, 
     32    Column('name', String, primary_key=True)) 
     33category_table.create() 
     34 
     35person_category_table = Table('person_category', meta, 
     36    Column('person_id', Integer, ForeignKey('person.id')), 
     37    Column('category_name', String, ForeignKey('category.name'))) 
     38person_category_table.create() 
     39 
     40person_person_table = Table('person_person', meta, 
     41    Column('person_id1', Integer, ForeignKey('person.id')), 
     42    Column('person_id2', Integer, ForeignKey('person.id'))) 
     43person_person_table.create() 
     44 
    3145elixir.delay_setup = True 
     46elixir.options_defaults.update(dict(autoload=True, shortnames=True)) 
    3247 
    3348class Person(Entity): 
    34 #    has_field('name', Unicode(32)) 
    35      
    3649    belongs_to('father', of_kind='Person', colname='father_id') 
    3750    has_many('children', of_kind='Person') 
    3851    has_many('pets', of_kind='Animal', inverse='owner') 
    3952    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') 
    4257 
    4358    def __str__(self): 
     
    4762        return s 
    4863 
     64 
    4965class Animal(Entity): 
    50 #    has_field('name', String(15)) 
    51 #    has_field('color', String(15)) 
    52      
    5366    belongs_to('owner', of_kind='Person', colname='owner_id') 
    5467    belongs_to('feeder', of_kind='Person', colname='feeder_id') 
    5568 
    56     using_options(autoload=True, shortnames=True) 
     69 
     70class Category(Entity): 
     71    has_and_belongs_to_many('persons', of_kind='Person',  
     72                            tablename='person_category') 
    5773 
    5874elixir.delay_setup = False 
     75elixir.options_defaults.update(dict(autoload=False, shortnames=False)) 
    5976 
    6077#----------- 
     
    92109        lisa = Person(name="Lisa") 
    93110         
    94         grampa.children.append(homer)         
     111        grampa.children.append(homer) 
    95112        homer.children.append(bart) 
    96113        lisa.father = homer 
     
    109126        assert p is Person.get_by(name="Lisa").father 
    110127 
     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 
    111167if __name__ == '__main__': 
    112168    test = TestAutoload()