Changeset 356

Show
Ignore:
Timestamp:
07/08/08 11:07:14 (4 years ago)
Author:
ged
Message:
  • better support for viewonly relationships
  • restore flush method on Entity (accidentaly renamed in r349... ooops)
    (closes ticket #54)
  • Added support for filtered OneToMany relationships. Produce viewonly
    relations. See source:elixir/trunk/tests/test_o2m.py for an example.
  • optimized "inverse" property on relation a bit (if None is found once, it's
    stored and not recomputed over and over)
  • embryonic debugging output
Location:
elixir/trunk
Files:
6 modified

Legend:

Unmodified
Added
Removed
  • elixir/trunk/CHANGES

    r355 r356  
    2323- Added on_reconstitute event/method decorator. Only works with SA 0.5. 
    2424- Added support for viewonly relationships (OneToMany and OneToOne). 
     25- Added support for filtered OneToMany relationships. Produce viewonly 
     26  relations. See source:elixir/trunk/tests/test_o2m.py for an example. 
    2527 
    2628Changes: 
  • elixir/trunk/TODO

    r347 r356  
    8484  instead of the acts_as_taggable Jonathan demonstrated 
    8585 
    86 - implement something like: 
    87  
    88     class A(Entity): 
    89         has_many('b', of_kind='B') 
    90         has_many('b_filtered', of_kind='B', filter=lambda c: c.extra < 10) 
    91     class B(Entity): 
    92         has_field('extra', Integer) 
    93         belongs_to('a', of_kind='A') 
    94  
    95   this is the more or less the same as what is described at: 
     86- support custom selectable on relationships, as is done at: 
    9687 
    9788  http://spyced.blogspot.com/2007/01/why-sqlalchemy-impresses-me.html 
  • elixir/trunk/elixir/__init__.py

    r349 r356  
    165165    Optionally drops the tables. 
    166166    ''' 
     167    session.rollback() 
     168    session.clear() 
     169    session.close() 
     170 
     171    cleanup_entities(entities) 
     172 
     173    sqlalchemy.orm.clear_mappers() 
     174    entities._entities = {} 
     175    del entities[:] 
     176 
    167177    if drop_tables: 
    168178        drop_all(*args, **kwargs) 
    169  
    170     cleanup_entities(entities) 
    171179 
    172180    for md in metadatas: 
     
    174182    metadatas.clear() 
    175183 
    176     session.close() 
    177184 
    178     sqlalchemy.orm.clear_mappers() 
    179     del entities[:] 
    180  
  • elixir/trunk/elixir/entity.py

    r350 r356  
    2222from elixir.properties import Property 
    2323 
     24DEBUG = False 
    2425 
    2526__doc_all__ = ['Entity', 'EntityMeta'] 
    26  
    2727 
    2828 
     
    454454                                (col.key, table.name)) 
    455455            table.append_column(col) 
     456            if DEBUG: 
     457                print "table.append_column(%s)" % col 
    456458 
    457459    def add_constraint(self, constraint): 
     
    478480        if mapper: 
    479481            mapper.add_property(name, property) 
     482            if DEBUG: 
     483                print "mapper.add_property('%s', %s)" % (name, repr(property)) 
    480484 
    481485    def add_mapper_extension(self, extension): 
     
    497501        return None 
    498502 
    499     def get_inverse_relation(self, rel, reverse=False): 
     503    def get_inverse_relation(self, rel, check_reverse=True): 
    500504        ''' 
    501505        Return the inverse relation of rel, if any, None otherwise. 
     
    504508        matching_rel = None 
    505509        for other_rel in self.relationships: 
    506             if other_rel.is_inverse(rel): 
     510            if rel.is_inverse(other_rel): 
    507511                if matching_rel is None: 
    508512                    matching_rel = other_rel 
     
    518522        # of the method though. But we do need to be careful not to start an 
    519523        # infinite recursive loop. 
    520         if matching_rel and not reverse: 
    521             rel.entity._descriptor.get_inverse_relation(matching_rel, True) 
     524        if matching_rel and check_reverse: 
     525            rel.entity._descriptor.get_inverse_relation(matching_rel, False) 
    522526 
    523527        return matching_rel 
     
    921925 
    922926    # session methods 
    923     def commit(self, *args, **kwargs): 
    924         return object_session(self).commit([self], *args, **kwargs) 
     927    def flush(self, *args, **kwargs): 
     928        return object_session(self).flush([self], *args, **kwargs) 
    925929 
    926930    def delete(self, *args, **kwargs): 
     
    951955        return self._global_session.update(self, *args, **kwargs) 
    952956 
     957    # only exist in SA < 0.5 
     958    # IMO, the replacement (session.add) doesn't sound good enough to be added 
     959    # here. For example: "o = Order(); o.add()" is not very telling. It's 
     960    # better to leave it as "session.add(o)" 
    953961    def save_or_update(self, *args, **kwargs): 
    954962        return self._global_session.save_or_update(self, *args, **kwargs) 
  • elixir/trunk/elixir/relationships.py

    r355 r356  
    356356 
    357357        self._target = None 
    358         self._inverse = None 
    359358 
    360359        self.property = None # sqlalchemy property 
     
    395394            return 
    396395 
    397         kwargs = {} 
    398         if self.inverse: 
     396        kwargs = self.get_prop_kwargs() 
     397 
     398        # viewonly relationships need to create "standalone" relations (ie 
     399        # shouldn't be a backref of another relation). 
     400        if self.inverse and not kwargs.get('viewonly', False): 
    399401            # check if the inverse was already processed (and thus has already 
    400402            # defined a backref we can use) 
    401403            if self.inverse.backref: 
    402                 kwargs['backref'] = self.inverse.backref 
     404                # let the user override the backref argument 
     405                if 'backref' not in kwargs: 
     406                    kwargs['backref'] = self.inverse.backref 
    403407            else: 
    404                 kwargs = self.get_prop_kwargs() 
    405  
    406408                # SQLAlchemy doesn't like when 'secondary' is both defined on 
    407409                # the relation and the backref 
     
    411413                self.backref = backref(self.name, **kwargs) 
    412414                return 
    413  
    414         kwargs.update(self.get_prop_kwargs()) 
    415415 
    416416        self.property = relation(self.target, **kwargs) 
     
    428428 
    429429    def inverse(self): 
    430         if not self._inverse: 
     430        if not hasattr(self, '_inverse'): 
    431431            if self.inverse_name: 
    432432                desc = self.target._descriptor 
     
    439439                assert self.match_type_of(inverse) 
    440440            else: 
    441                 inverse = self.target._descriptor.get_inverse_relation(self) 
    442  
    443             if inverse: 
    444                 self._inverse = inverse 
     441                check_reverse = not self.kwargs.get('viewonly', False) 
     442                inverse = self.target._descriptor.get_inverse_relation(self, 
     443                            check_reverse=check_reverse) 
     444 
     445            self._inverse = inverse 
     446            if inverse and not self.kwargs.get('viewonly', False): 
    445447                inverse._inverse = self 
    446448 
     
    452454 
    453455    def is_inverse(self, other): 
    454         # viewonly relationships shouldn't match as inverse of anything (so 
    455         # that no backref is created -- which doesn't make sense in that case) 
    456         viewonly = self.kwargs.get('viewonly', False) or \ 
    457                    other.kwargs.get('viewonly', False) 
    458         return not viewonly and \ 
     456        # viewonly relationships are not symmetrical: a viewonly relationship 
     457        # should have exactly one inverse (a ManyToOne relationship), but that 
     458        # inverse shouldn't have the viewonly relationship as its inverse. 
     459        return not other.kwargs.get('viewonly', False) and \ 
    459460               other is not self and \ 
    460461               self.match_type_of(other) and \ 
     
    614615    uselist = False 
    615616 
     617    def __init__(self, *args, **kwargs): 
     618        self.filter = kwargs.pop('filter', None) 
     619        if self.filter is not None: 
     620            kwargs['viewonly'] = True 
     621        super(OneToOne, self).__init__(*args, **kwargs) 
     622 
    616623    def match_type_of(self, other): 
    617624        return isinstance(other, ManyToOne) 
    618625 
    619626    def create_keys(self, pk): 
    620         # When using a viewonly relationship, you are on your own: Elixir 
    621         # doesn't check that a corresponding ManyToOne relationship exists. 
    622         if self.kwargs.get('viewonly', False): 
    623             return 
    624  
    625627        # make sure an inverse relationship exists 
    626628        if self.inverse is None: 
     
    648650            kwargs['remote_side'] = self.inverse.foreign_key 
    649651 
    650         # viewonly relationships do not have any inverse (and they provide  
    651         # their primaryjoin argument manually anyway). 
    652         if not self.kwargs.get('viewonly', False): 
    653             if self.inverse.primaryjoin_clauses: 
    654                 kwargs['primaryjoin'] = and_(*self.inverse.primaryjoin_clauses) 
     652        joinclauses = self.inverse.primaryjoin_clauses 
     653        if self.filter: 
     654            joinclauses.append(self.filter(self.target.table.c)) 
     655        if joinclauses: 
     656            kwargs['primaryjoin'] = and_(*joinclauses) 
    655657 
    656658        kwargs.update(self.kwargs) 
  • elixir/trunk/tests/test_o2m.py

    r355 r356  
    108108 
    109109    def test_viewonly(self): 
     110        # the callable primaryjoin seem to be unstable... sometime it works, 
     111        # sometime it doesn't... for no apparent reason. I think it's a bug in 
     112        # the current revision of SQLAlchemy I'm using (4900). 
    110113        class User(Entity): 
    111114            two_blurbs = OneToMany('Blurb', primaryjoin=lambda: 
     
    113116                viewonly=True 
    114117            ) 
     118            blurbs = OneToMany('Blurb', 
     119                               collection_class=ordering_list('position'), 
     120                               order_by='position') 
     121 
     122        class Blurb(Entity): 
     123            user = ManyToOne('User') 
     124            position = Field(Integer) 
     125            blurb = Field(Unicode(255)) 
     126 
     127            def __init__(self, blurb, **kwargs): 
     128                super(Blurb, self).__init__(blurb=blurb, **kwargs) 
     129 
     130            def __repr__(self): 
     131                return 'Blurb(%r, %r)' % (self.position, self.blurb) 
     132 
     133        setup_all(True) 
     134 
     135        user = User(blurbs=[Blurb(u'zero'), Blurb(u'one'), Blurb(u'two')]) 
     136 
     137        session.commit() 
     138        session.clear() 
     139 
     140        user = User.get(1) 
     141        assert len(user.two_blurbs) == 2 
     142        assert user.two_blurbs[0].blurb == 'zero' 
     143        assert user.two_blurbs[1].blurb == 'one' 
     144 
     145    def test_filtered(self): 
     146        class User(Entity): 
     147            two_blurbs = OneToMany('Blurb', filter=lambda c: c.position < 2) 
    115148            blurbs = OneToMany('Blurb', 
    116149                               collection_class=ordering_list('position'),