Changeset 199

Show
Ignore:
Timestamp:
09/06/07 15:55:47 (6 years ago)
Author:
ged
Message:
  • Added test for the case when you refer to a remotely-defined class by its
    named after importing it into the local namespace.
  • Implemented a new syntax to declare fields and relationships, much closer to
    what is found in other Python ORM's. The with_fields syntax is now
    deprecated in favor of a that new syntax. The old statement-based (has_field et
    al.) syntax stays the default for now. This was done with help from a patch
    by Adam Gomaa.
  • Relationships to other classes can now also be defined using the classes
    themselves in addition to the class names. Obviously, this doesn't work for
    forward references.
Location:
elixir/trunk
Files:
1 added
9 modified

Legend:

Unmodified
Added
Removed
  • elixir/trunk/CHANGES

    r198 r199  
    33  non-polymorphic multi-table (aka joined table) inheritance. 
    44- Made the statement system more powerfull. 
     5- Implemented a new syntax to declare fiels and relationships, much closer to 
     6  what is found in other Python ORM's. The with_fields syntax is now 
     7  deprecated in favor of a that new syntax. The old statement based (has_field et 
     8  al.) syntax stays the default for now. This was done with help from a patch 
     9  by Adam Gomaa. 
     10- Relationships to other classes can now also be defined using the classes 
     11  themselves in addition to the class namees. Obviously, this doesn't work for 
     12  forward references. 
    513- Autodelay and init order changed => order_by + belongs_to, belongs_to + pk,  
    614  ... 
  • elixir/trunk/docs/tutorial.rst

    r59 r199  
    3838 
    3939    class Movie(Entity): 
    40         with_fields( 
    41             title = Field(Unicode(30)), 
    42             year = Field(Integer), 
    43             description = Field(Unicode) 
    44         ) 
     40        title = Field(Unicode(30)) 
     41        year = Field(Integer) 
     42        description = Field(Unicode) 
    4543         
    4644        def __repr__(self): 
     
    6260easily trace what is happening in an interactive python interpreter. 
    6361 
    64 Also, please note that elixir currently provide two different ways to declare 
    65 the fields on your entities. We have not decided yet on which one we like best, 
    66 or if we will always keep both. The other way to declare your fields is using 
    67 the ``has_field`` statement, rather than the ``with_fields`` statement.  The 
    68 ``Movie`` example above can be declared using the ``has_field`` statement like 
    69 so: 
     62Also, please note that elixir currently provide two different ways to 
     63declare the fields on your entities. We have not decided yet on which 
     64one we like best, or if we will always keep both. The other way to 
     65declare your fields is using the ``has_field`` statement, rather than 
     66assigning directly to the class attributes.  The ``Movie`` example 
     67above could be declared using the ``has_field`` statement like so: 
    7068 
    7169:: 
     
    151149 
    152150    class Genre(Entity): 
    153         with_fields( 
    154             name = Field(Unicode(15), unique=True) 
    155         ) 
     151        name = Field(Unicode(15), unique=True) 
     152 
    156153         
    157154        def __repr__(self): 
     
    169166 
    170167    class Movie(Entity): 
    171         with_fields( 
    172             title = Field(Unicode(30)), 
    173             year = Field(Integer), 
    174             description = Field(Unicode) 
    175         ) 
     168        title = Field(Unicode(30)), 
     169        year = Field(Integer), 
     170        description = Field(Unicode) 
    176171         
    177172        belongs_to('genre', of_kind='Genre')                # add this line 
     
    182177     
    183178    class Genre(Entity): 
    184         with_fields( 
    185             name = Field(Unicode(15)) 
    186         ) 
     179        name = Field(Unicode(15)) 
    187180         
    188181        has_many('movies', of_kind='Movie')                 # and this one 
  • elixir/trunk/elixir/__init__.py

    r196 r199  
    2828from elixir.relationships import belongs_to, has_one, has_many, \ 
    2929                                 has_and_belongs_to_many 
     30from elixir.relationships import ManyToOne, OneToOne, OneToMany, ManyToMany 
    3031from elixir.properties import has_property 
    3132from elixir.statements import Statement 
     
    4142           'has_property',  
    4243           'belongs_to', 'has_one', 'has_many', 'has_and_belongs_to_many', 
     44           'ManyToOne', 'OneToOne', 'OneToMany', 'ManyToMany', 
    4345           'using_options', 'using_table_options', 'using_mapper_options', 
    4446           'options_defaults', 'metadata', 'objectstore', 
  • elixir/trunk/elixir/entity.py

    r197 r199  
    22Entity baseclass, metaclass and descriptor 
    33''' 
     4 
     5import sqlalchemy 
    46 
    57from sqlalchemy                     import Table, Integer, String, desc,\ 
     
    911from sqlalchemy.ext.sessioncontext  import SessionContext 
    1012from sqlalchemy.util                import OrderedDict 
    11 import sqlalchemy 
     13 
    1214from elixir.statements              import Statement 
    1315from elixir.fields                  import Field 
     
    6264 
    6365        self.fields = OrderedDict() 
    64         self.relationships = OrderedDict() 
     66        self.relationships = list() 
    6567        self.delayed_properties = dict() 
    6668        self.constraints = list() 
     
    120122        since a loop of primary_keys is not a valid situation. 
    121123        """ 
    122         for rel in self.relationships.itervalues(): 
     124        for rel in self.relationships: 
    123125            rel.create_keys(True) 
    124126 
     
    153155 
    154156    def setup_relkeys(self): 
    155         for rel in self.relationships.itervalues(): 
     157        for rel in self.relationships: 
    156158            rel.create_keys(False) 
    157159 
     
    217219 
    218220    def setup_reltables(self): 
    219         for rel in self.relationships.itervalues(): 
     221        for rel in self.relationships: 
    220222            rel.create_tables() 
    221223 
     
    367369 
    368370    def setup_properties(self): 
    369         for rel in self.relationships.itervalues(): 
     371        for rel in self.relationships: 
    370372            rel.create_properties() 
    371373 
     
    417419 
    418420        matching_rel = None 
    419         for other_rel in self.relationships.itervalues(): 
     421        for other_rel in self.relationships: 
    420422            if other_rel.is_inverse(rel): 
    421423                if matching_rel is None: 
     
    437439        return matching_rel 
    438440 
     441    def find_relationship(self, name): 
     442        for rel in self.relationships: 
     443            if rel.name == name: 
     444                return rel 
     445        if self.parent: 
     446            return self.parent.find_relationship(name) 
     447        else: 
     448            return None 
     449 
    439450    def primary_keys(self): 
    440451        if self.autoload: 
     
    448459    primary_keys = property(primary_keys) 
    449460 
    450     def all_relationships(self): 
    451         if self.parent: 
    452             res = self.parent._descriptor.all_relationships 
    453         else: 
    454             res = dict() 
    455         res.update(self.relationships) 
    456         return res 
    457     all_relationships = property(all_relationships) 
    458  
    459461 
    460462class TriggerProxy(object): 
     463    """A class that serves as a "trigger" ; accessing its attributes runs 
     464    the function that is set at initialization. 
     465 
     466    Primarily used for setup_all(). 
     467 
     468    Note that the `setupfunc` parameter is called on each access of 
     469    the attribute. 
     470 
     471    """ 
    461472    def __init__(self, class_, attrname, setupfunc): 
    462473        self.class_ = class_ 
     
    472483        proxied_attr = getattr(self.class_, self.attrname) 
    473484        return "<TriggerProxy (%s)>" % (self.class_.__name__) 
     485 
     486def _is_entity(class_): 
     487    return isinstance(class_, EntityMeta) 
    474488 
    475489class EntityMeta(type): 
     
    495509 
    496510        # Append all entities which are currently visible by the entity. This  
    497         # will find more entities only if some of them where imported from another 
    498         # module. 
     511        # will find more entities only if some of them where imported from  
     512        # another module. 
    499513        for entity in [e for e in caller_frame.f_locals.values()  
    500                          if e.__class__.__name__ == 'EntityMeta']: 
     514                         if _is_entity(e)]: 
    501515            caller_entities[entity.__name__] = entity 
    502516 
     
    506520        # process statements. Needed before the proxy for metadata 
    507521        Statement.process(cls) 
     522 
     523        # Process attributes, for the assignment syntax. 
     524        cls._process_attrs(dict_) 
    508525 
    509526        # setup misc options here (like tablename etc.) 
     
    513530        # TODO: support entity_name... or maybe not. I'm not sure it makes  
    514531        # sense in Elixir. 
    515         cls.setup_proxy() 
    516  
    517     def setup_proxy(cls, entity_name=None): 
     532        cls._setup_proxy() 
     533 
     534    def _setup_proxy(cls, entity_name=None): 
    518535        #TODO: move as much as possible of those "_private" values to the 
    519536        # descriptor, so that we don't mess the initial class. 
     
    555572        cls._ready = True 
    556573 
     574    def _process_attrs(cls, attr_dict): 
     575        """Process class attributes, looking for Elixir `Field`s or 
     576        `Relationship`. 
     577        """ 
     578 
     579        for name, attr in attr_dict.iteritems(): 
     580            # Check if it's Elixir related.  
     581            if isinstance(attr, Field): 
     582                # If no colname was defined (through the 'colname' kwarg), set 
     583                # it to the name of the attr. 
     584                if attr.colname is None: 
     585                    attr.colname = name 
     586                cls._descriptor.add_field(attr) 
     587            elif isinstance(attr, elixir.relationships.Relationship): 
     588                attr.name = name  
     589                attr.entity = cls 
     590                cls._descriptor.relationships.append(attr) 
     591            else: 
     592                # Not an Elixir field, let it be.  
     593                pass 
     594        return 
     595 
    557596    def __getattribute__(cls, name): 
    558597        if type.__getattribute__(cls, "_ready"): 
     
    593632    tutorial. 
    594633    ''' 
    595  
    596634    __metaclass__ = EntityMeta 
    597635 
  • elixir/trunk/elixir/ext/associable.py

    r195 r199  
    131131            self.type = tablename 
    132132    
    133     class Associable(el.relationships.Relationship): 
     133    class Associable(object): 
    134134        """An associable Elixir Statement object""" 
    135135        def __init__(self, entity, name=None, uselist=True, lazy=True): 
     
    142142            else: 
    143143                self.name = name 
    144             self.entity._descriptor.relationships[able_name] = self 
    145  
    146         def create_keys(self, pk): 
    147             if pk: 
    148                 return 
     144 
     145        def after_table(self): 
    149146            field = el.Field(sa.Integer, sa.ForeignKey('%s.id' % able_name), 
    150147                             colname='%s_assoc_id' % interface_name) 
    151148            self.entity._descriptor.add_field(field) 
    152149 
    153         def create_tables(self): 
    154150            if not hasattr(assoc_entity, '_assoc_table'): 
    155151                association_table = sa.Table("%s" % able_name, assoc_entity._descriptor.metadata, 
     
    178174                }) 
    179175         
    180         def create_properties(self): 
    181176            entity = self.entity 
    182177            entity.mapper.add_property( 
  • elixir/trunk/elixir/fields.py

    r196 r199  
    88This module contains DSL statements which allow you to declare which  
    99fields (columns) your Elixir entities have.  There are currently two 
    10 different statements that you can use to declare fields: 
     10different ways to declare your entities fields: through the has_field_ 
     11statement, and by using the `Object-oriented syntax`_. Note that the  
     12with_fields_ statement is currently deprecated in favor of the  
     13`Object-oriented syntax`_. 
    1114 
    1215 
    13 `has_field` 
    14 ----------- 
     16has_field 
     17--------- 
    1518The `has_field` statement allows you to define fields one at a time. 
    1619 
     
    6568        has_field('name', String(50)) 
    6669 
     70Object-oriented syntax 
     71---------------------- 
    6772 
    68 `with_fields` 
    69 ------------- 
    70 The `with_fields` statement allows you to define fields all at once. 
     73Here is a quick example of how to use the object-oriented syntax. 
    7174 
     75:: 
     76 
     77    class Person(Entity): 
     78        id = Field(Integer, primary_key=True) 
     79        name = Field(String(50)) 
     80 
     81with_fields 
     82----------- 
     83The `with_fields` statement is **deprecated** in favor of the `Object-oriented 
     84syntax`_. It allows you to define all fields of an entity at once.  
    7285Each keyword argument to this statement represents one field, which should 
    7386be a `Field` object. The first argument to a Field object is its type.  
  • elixir/trunk/elixir/relationships.py

    r198 r199  
    239239    ''' 
    240240     
    241     def __init__(self, entity, name, *args, **kwargs): 
    242         self.entity = entity 
    243         self.name = name 
     241    def __init__(self, of_kind, *args, **kwargs): 
     242        self.entity = None 
     243        self.name = None 
    244244        self.inverse_name = kwargs.pop('inverse', None) 
    245245 
    246         if 'through' in kwargs and 'via' in kwargs: 
    247             setattr(entity, name,  
    248                     association_proxy(kwargs.pop('through'), kwargs.pop('via'), 
    249                                       **kwargs)) 
    250             return 
    251         elif 'through' in kwargs or 'via' in kwargs: 
    252             raise Exception("'through' and 'via' relationship keyword " 
    253                             "arguments should be used in combination.") 
    254  
    255         self.of_kind = kwargs.pop('of_kind') 
     246        self.of_kind = of_kind 
    256247 
    257248        self._target = None 
     
    265256        self.kwargs = kwargs 
    266257 
    267         self.entity._descriptor.relationships[self.name] = self 
    268258     
    269259    def create_keys(self, pk): 
     
    310300    def target(self): 
    311301        if not self._target: 
    312             path = self.of_kind.rsplit('.', 1) 
    313             classname = path.pop() 
    314  
    315             if path: 
    316                 # do we have a fully qualified entity name? 
    317                 module = sys.modules[path.pop()] 
    318                 self._target = getattr(module, classname, None) 
     302            if isinstance(self.of_kind, EntityMeta): 
     303                self._target = self.of_kind 
    319304            else: 
    320                 # If not, try the list of entities of the "caller" of the  
    321                 # source class. Most of the time, this will be the module the 
    322                 # class is defined in. But it could also be a method (inner 
    323                 # classes). 
    324                 caller_entities = EntityMeta._entities[self.entity._caller] 
    325                 self._target = caller_entities[classname] 
     305                path = self.of_kind.rsplit('.', 1) 
     306                classname = path.pop() 
     307 
     308                if path: 
     309                    # do we have a fully qualified entity name? 
     310                    module = sys.modules[path.pop()] 
     311                    self._target = getattr(module, classname, None) 
     312                else: 
     313                    # If not, try the list of entities of the "caller" of the  
     314                    # source class. Most of the time, this will be the module  
     315                    # the class is defined in. But it could also be a method  
     316                    # (inner classes). 
     317                    caller_entities = EntityMeta._entities[self.entity._caller] 
     318                    self._target = caller_entities[classname] 
    326319        return self._target 
    327320    target = property(target) 
     
    331324            if self.inverse_name: 
    332325                desc = self.target._descriptor 
    333                 # we use all_relationships so that relationships from parent 
    334                 # entities are included too 
    335                 inverse = desc.all_relationships.get(self.inverse_name, None) 
     326                inverse = desc.find_relationship(self.inverse_name) 
    336327                if inverse is None: 
    337328                    raise Exception( 
     
    362353 
    363354 
    364 class BelongsTo(Relationship): 
     355class ManyToOne(Relationship): 
    365356    ''' 
    366357     
    367358    ''' 
    368359     
    369     def __init__(self, entity, name, *args, **kwargs): 
     360    def __init__(self, *args, **kwargs): 
    370361        self.colname = kwargs.pop('colname', []) 
    371362        if self.colname and not isinstance(self.colname, list): 
     
    389380        self.foreign_key = list() 
    390381        self.primaryjoin_clauses = list() 
    391         super(BelongsTo, self).__init__(entity, name, *args, **kwargs) 
     382        super(ManyToOne, self).__init__(*args, **kwargs) 
    392383     
    393384    def match_type_of(self, other): 
    394         return isinstance(other, (HasMany, HasOne)) 
     385        return isinstance(other, (OneToMany, OneToOne)) 
    395386 
    396387    def create_keys(self, pk): 
     
    497488 
    498489 
    499 class HasOne(Relationship): 
     490class OneToOne(Relationship): 
    500491    uselist = False 
    501492 
    502493    def match_type_of(self, other): 
    503         return isinstance(other, BelongsTo) 
     494        return isinstance(other, ManyToOne) 
    504495 
    505496    def create_keys(self, pk): 
     
    537528 
    538529 
    539 class HasMany(HasOne): 
     530class OneToMany(OneToOne): 
    540531    uselist = True 
    541532     
    542533    def get_prop_kwargs(self): 
    543         kwargs = super(HasMany, self).get_prop_kwargs() 
     534        kwargs = super(OneToMany, self).get_prop_kwargs() 
    544535 
    545536        if 'order_by' in kwargs: 
     
    551542 
    552543 
    553 class HasAndBelongsToMany(Relationship): 
     544class ManyToMany(Relationship): 
    554545    uselist = True 
    555546 
    556     def __init__(self, entity, name, *args, **kwargs): 
     547    def __init__(self, *args, **kwargs): 
    557548        self.user_tablename = kwargs.pop('tablename', None) 
    558549        self.local_side = kwargs.pop('local_side', []) 
     
    565556        self.primaryjoin_clauses = list() 
    566557        self.secondaryjoin_clauses = list() 
    567         super(HasAndBelongsToMany, self).__init__(entity, name,  
    568                                                   *args, **kwargs) 
     558        super(ManyToMany, self).__init__(*args, **kwargs) 
    569559 
    570560    def match_type_of(self, other): 
    571         return isinstance(other, HasAndBelongsToMany) 
     561        return isinstance(other, ManyToMany) 
    572562 
    573563    def create_tables(self): 
     
    718708 
    719709    def is_inverse(self, other): 
    720         return super(HasAndBelongsToMany, self).is_inverse(other) and \ 
     710        return super(ManyToMany, self).is_inverse(other) and \ 
    721711               (self.user_tablename == other.user_tablename or  
    722712                (not self.user_tablename and not other.user_tablename)) 
     
    776766 
    777767 
    778 belongs_to = Statement(BelongsTo) 
    779 has_one = Statement(HasOne) 
    780 has_many = Statement(HasMany) 
    781 has_and_belongs_to_many = Statement(HasAndBelongsToMany) 
     768def rel_statement_handler(target): 
     769    class Handler(object): 
     770        def __init__(self, entity, name, *args, **kwargs): 
     771            if 'through' in kwargs and 'via' in kwargs: 
     772                setattr(entity, name,  
     773                        association_proxy(kwargs.pop('through'),  
     774                                          kwargs.pop('via'), 
     775                                          **kwargs)) 
     776                return 
     777            elif 'through' in kwargs or 'via' in kwargs: 
     778                raise Exception("'through' and 'via' relationship keyword " 
     779                                "arguments should be used in combination.") 
     780            rel = target(kwargs.pop('of_kind'), *args, **kwargs) 
     781            rel.name = name 
     782            rel.entity = entity 
     783            entity._descriptor.relationships.append(rel) 
     784    return Handler 
     785 
     786 
     787belongs_to = Statement(rel_statement_handler(ManyToOne)) 
     788has_one = Statement(rel_statement_handler(OneToOne)) 
     789has_many = Statement(rel_statement_handler(OneToMany)) 
     790has_and_belongs_to_many = Statement(rel_statement_handler(ManyToMany)) 
  • elixir/trunk/tests/b.py

    r147 r199  
    33class B(Entity): 
    44    has_field('name', String(30)) 
    5     has_many('a', of_kind='tests.a.A') 
     5    has_many('as_', of_kind='tests.a.A') 
    66 
  • elixir/trunk/tests/test_packages.py

    r178 r199  
    44 
    55from elixir import * 
    6 import elixir 
     6import sys 
    77 
    8 def teardown(): 
    9     cleanup_all() 
     8def setup(self): 
     9    metadata.bind = 'sqlite:///' 
    1010 
    1111class TestPackages(object): 
    12     def setup(self): 
    13         metadata.bind = 'sqlite:///' 
    14      
    1512    def teardown(self): 
    16         drop_all() 
    17         objectstore.clear() 
     13        cleanup_all(True) 
    1814     
    1915    def test_packages(self): 
    2016        # This is an ugly workaround because when nosetest is run globally (ie 
    2117        # either on the tests directory or in the "trunk" directory, it imports 
    22         # all modules, including a and b and thus their entities are 
    23         # immediately setup but then the other tests clear all mappers, and  
    24         # when we get here, this tests doesn't reinit those because the modules 
    25         # are not reimported.  
    26         # In short, one more reason to use delay_setup by default.  
    27         # Note that even if we set delay setup in this particular test, before 
    28         # the module imports, it'll fail because we'd need to set delay_setup 
    29         # before the a and b modules are imported by nosetests. 
    30         import sys 
     18        # all modules, including a and b. Then when any other test calls 
     19        # setup_all(), A and B are also setup, but then the other test also 
     20        # calls cleanup_all(), so when we get here, A and B are already dead and 
     21        # reimporting their modules does nothing because they were already 
     22        # imported. 
    3123        sys.modules.pop('tests.a', None) 
    3224        sys.modules.pop('tests.b', None) 
     
    3426        from tests.a import A 
    3527        from tests.b import B 
    36         create_all() 
    3728 
    38         a = A(name='a1') 
    39         b = B(name='b1') 
     29        setup_all(True) 
    4030 
    41         b.a.append(a) 
     31        b1 = B(name='b1', as_=[A(name='a1')]) 
    4232 
    4333        objectstore.flush() 
     34        objectstore.clear() 
    4435 
     36        a = A.query().one() 
     37 
     38        assert a in a.b.as_ 
     39 
     40    def test_ref_to_imported_entity_using_class(self): 
     41        sys.modules.pop('tests.a', None) 
     42        sys.modules.pop('tests.b', None) 
     43 
     44        from tests.a import A 
     45        from tests.b import B 
     46 
     47        class C(Entity): 
     48            has_field('name', String(30)) 
     49            belongs_to('a', of_kind=A) 
     50 
     51        setup_all(True) 
     52 
     53        # 'a_id' in ... is not supported before SA 0.4 
     54        assert C.table.columns.has_key('a_id') 
     55 
     56    def test_ref_to_imported_entity_using_name(self): 
     57        sys.modules.pop('tests.a', None) 
     58        sys.modules.pop('tests.b', None) 
     59 
     60        from tests.a import A 
     61        from tests.b import B 
     62 
     63        class C(Entity): 
     64            has_field('name', String(30)) 
     65            belongs_to('a', of_kind='A') 
     66 
     67        setup_all(True) 
     68 
     69        assert C.table.columns.has_key('a_id') 
     70