Changeset 3550

Show
Ignore:
Timestamp:
09/12/07 16:53:26 (1 year ago)
Author:
mscott
Message:

Merge #65 to trunk.

schevo.test.test_field_entity, schevo.test.test_field_entitylist, schevo.test.test_field_entityset

  • Add tests for specifying default values for Entity, EntityList, and EntitySet fields.

schevo.transaction:resolve

  • Add docstring.
  • Move out of schevo.transaction:_Populate body.
  • Add db argument.
  • Raise a ValueError if value could not be found when attempting resolution.
  • Make field_names optional, since it's only applicable for generating friendly error messages during initial/sample data population.

schevo.transaction:_Populate

  • Short-circuit if no data is given to populate. This skips any sort of default-value resolution when it is not strictly necessary.
  • Move process_data function to a private instancemethod, rather than an inline function defined within _execute.

SchevoSchemaDefinition

  • better documentation for initial/sample data
  • documentation for field default values
Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/Schevo/doc/SchevoSchemaDefinition.txt

    r3258 r3550  
    77 
    88.. contents:: 
     9 
     10.. Quick reference: Order of section delineators is === --- ... ~~~ 
    911 
    1012 
     
    326328------------------- 
    327329 
    328 Initial and sample data for a child entity can be supplied by 
    329 specifying the value of the first key of the parent.  For example:: 
     330Initial data is data that Schevo uses when creating a new database. 
     331Initial data is *always* processed during new database creation. 
     332Specify initial data for an extent by assigning an `_initial` 
     333attribute to the entity class in your database schema. 
     334 
     335Sample data is data that Schevo uses to populate a database after its 
     336creation.  Sample data is only processed if the ``-p`` or ``--sample`` 
     337option is passed to the ``schevo db create`` command-line tool, or if 
     338you call the `populate` method on an open database.  Specify sample 
     339data for an extent by assigning a `_sample` attribute to the entity 
     340class in your database schema. 
     341 
     342Unit test sample data is data that Schevo populates a database with 
     343when running a suite of unit tests against a database schema.  Specify 
     344unit test sample data for an extent by assigning a 
     345`_sample_unittest` attribute to the entity class in your database 
     346schema. 
     347 
     348You may also specify sample data with a custom attribute name, such as 
     349`_sample_abc`. Populate a database with your custom sample data by 
     350passing the suffix to the call to `populate` on your database. 
     351For the example, to populate `db` with the `abc` sample data, call 
     352``db.populate('abc')``. 
     353 
     354Specify each collection of initial or sample data as a list of tuples, 
     355where each tuple contains values for the fields that the extent's 
     356`create` transaction expects, in the order that it expects 
     357them. Normally, those fields are the same as the fields specified in 
     358the entity class, but in complex database schemata the fields may 
     359differ. 
     360 
     361To specify the default value for a field, use the constant 
     362`DEFAULT`. For example, the `Foo` entities that have the names 
     363``'c'`` and ``'d'`` will have a `size` of ``5``:: 
    330364 
    331365    class Foo(E.Entity): 
    332366 
    333367        name = f.string() 
     368        size = f.integer(default=5) 
    334369 
    335370        _key(name) 
    336371 
    337372        _initial = [ 
    338             ('One', ), 
    339             ('Two', ), 
    340             ('Buckle', ), 
    341             ('Shoe', ), 
     373            ('a', 1), 
     374            ('b', 2), 
     375            ('c', DEFAULT), 
     376            ('d', DEFAULT), 
    342377            ] 
    343378 
    344     class FooChild(E.Entity): 
     379To specify a value for an `Entity` field that allows only one entity 
     380type, use a tuple containing the values of the fields of the first key 
     381of that entity type.  For example:: 
     382 
     383    class Foo(E.Entity): 
     384 
     385        name = f.string() 
     386        for_rainy_days = f.boolean() 
     387 
     388        _key(name, for_rainy_days) 
     389 
     390        _initial = [ 
     391            ('One', False), 
     392            ('Two', False), 
     393            ('Buckle', False), 
     394            ('Shoe', False), 
     395            ('Shoe', True), 
     396            ] 
     397 
     398    class Bar(E.Entity): 
    345399 
    346400        foo = f.entity('Foo') 
    347         bar = f.string() 
     401        baz = f.string() 
    348402 
    349403        _key(foo, bar) 
    350404 
    351405        _initial = [ 
    352             (('One',), 'This is how it starts.'), 
    353             (('Shoe',), 'This is how it ends.'), 
    354             (('Shoe',), 'Need to have two shoes.'), 
    355             (('Shoe',), 'And one for a rainy day.'), 
     406            (('One', False), 'This is how it starts.'), 
     407            (('Shoe', False), 'This is how it ends.'), 
     408            (('Shoe', False), 'Need to have two shoes.'), 
     409            (('Shoe', True), 'And one for a rainy day.'), 
    356410            ] 
    357411 
    358 The appearance of a tuple tells the data generator to find a ``Foo`` 
    359 entity whose first unique key matches the values supplied in the 
    360 tuple. 
     412To specify a value for an `Entity` field that allows more than one 
     413entity type, use a two-tuple that first gives the entity type, then 
     414gives a tuple of the values of the fields of the first key of that 
     415entity type.  For example:: 
     416 
     417    class Foo(E.Entity): 
     418 
     419        name = f.string() 
     420 
     421        _key(name) 
     422 
     423        _initial = [ 
     424            ('one',), 
     425            ('two',), 
     426            ] 
     427 
     428    class Bar(E.Entity): 
     429 
     430        number = f.integer() 
     431 
     432        _key(number) 
     433 
     434        _initial = [ 
     435            (1,), 
     436            (2,), 
     437            ] 
     438 
     439    class Baz(E.Entity): 
     440 
     441        foo_or_bar = f.entity('Foo', 'Bar') 
     442        notes = f.string() 
     443 
     444        _initial = [ 
     445            (('Foo', ('one',)), 'This is the Foo that is named one.'), 
     446            (('Foo', ('two',)), 'This is the Foo that is named two.'), 
     447            (('Bar', (1,)), 'This is the Bar that is numbered 1.'), 
     448            (('Bar', (2,)), 'This is the Bar that is numbered 2.'), 
     449            ] 
     450 
     451To specify values for `EntityList` and `EntitySet` fields, simply 
     452enclose the representations of entities within a list or a set, 
     453respectively. 
    361454 
    362455 
     
    380473        birthdate = f.date(required=False)        # [2] 
    381474        disposition = f.entity('Disposition', on_delete=UNASSIGN, 
    382                                required=False)    # [3] 
     475                               required=False, 
     476                               default=('Cheerful',))    # [3] 
    383477 
    3844781. The `name` field is a required `Unicode` field.  By default, all 
     
    388482 
    3894833. The `disposition` field is an optional `Entity` field that can 
    390    store a reference to a `Disposition` entity.  When the referenced 
    391    `Disposition` entity is deleted, this field's value is set to 
    392    `UNASSIGNED`. See also `cascading delete rules for Entity fields`_. 
     484   store a reference to a `Disposition` entity.   
     485 
     486   When the referenced `Disposition` entity is deleted, this field's 
     487   value is set to `UNASSIGNED`. See also `cascading delete rules for 
     488   Entity fields`_. 
     489 
     490   The default value of this field is the `Disposition` entity that 
     491   matches ``('Cheerful',)``.  See `Default field values`_ below. 
    393492 
    394493 
     
    412511safest operation, since it prevents accidental deletion of an entity 
    413512if it is being referred to by another entity. 
     513 
     514 
     515Default field values 
     516.................... 
     517 
     518Default field values are assigned to fields in `Create` transactions. 
     519 
     520Specify default field values in a field definition by giving either a 
     521default value in the same notation as you would for initial or sample 
     522data, or by specifying a callable that returns the actual value to be 
     523used as the default value. 
     524 
     525Default values are not assigned to a field if a value has been 
     526supplied for that field in the keyword arguments specified when 
     527creating the `Create` transaction. 
    414528 
    415529 
  • trunk/Schevo/schevo/test/test_field_entity.py

    r3513 r3550  
    1818 
    1919        thing = f.string() 
     20 
     21        _key(thing) 
    2022 
    2123        _sample_unittest = [ 
     
    3941    class Baz(E.Entity): 
    4042 
    41         foo = f.entity('Foo'
     43        foo = f.entity('Foo', default=('a', )
    4244        bar = f.entity('Bar', default=default_bar) 
    4345        foobar = f.entity('Foo', 'Bar', required=False) 
     
    5557        assert f.convert('Foo-1', db) == db.Foo[1] 
    5658        assert f.convert(u'Foo-1', db) == db.Foo[1] 
     59 
     60    def test_default(self): 
     61        tx = db.Baz.t.create() 
     62        assert tx.foo == db.Foo.findone(thing='a') 
     63        assert tx.bar == db.Bar.findone(stuff=1) 
    5764 
    5865    def test_reversible_valid_values(self): 
  • trunk/Schevo/schevo/test/test_field_entitylist.py

    r3519 r3550  
    1  
    21"""EntityList field unit tests. 
    32 
     
    7978 
    8079            _initial_priority = 1 
     80 
     81 
     82        class BooBoo(E.Entity): 
     83 
     84            foo_foos = f.entity_list('FooFoo', 
     85                                     default=[('foofoo2', ), ('foofoo1', )]) 
    8186 
    8287 
     
    124129                ] 
    125130        ''' 
     131 
     132    def test_default(self): 
     133        tx = db.BooBoo.t.create() 
     134        assert tx.foo_foos == [db.FooFoo.findone(name='foofoo2'), 
     135                               db.FooFoo.findone(name='foofoo1')] 
    126136 
    127137    def test_store_and_retrieve_UNASSIGNED(self): 
  • trunk/Schevo/schevo/test/test_field_entityset.py

    r3519 r3550  
    6060 
    6161            _initial_priority = 1 
     62 
     63 
     64        class BooBoo(E.Entity): 
     65 
     66            foo_foos = f.entity_set('FooFoo', 
     67                                    default=set([('foofoo2', ), ('foofoo1', )])) 
    6268 
    6369 
     
    105111                ] 
    106112        ''' 
     113 
     114    def test_default(self): 
     115        tx = db.BooBoo.t.create() 
     116        assert tx.foo_foos == set([db.FooFoo.findone(name='foofoo2'), 
     117                                   db.FooFoo.findone(name='foofoo1')]) 
    107118 
    108119    def test_store_and_retrieve_UNASSIGNED(self): 
  • trunk/Schevo/schevo/transaction.py

    r3519 r3550  
    208208            setattr(self, name, value) 
    209209        # Look for matching field values in objects passed as args. 
    210         for field_name, field in field_map.iteritems(): 
    211             if not field.assigned and not field.readonly: 
     210        for field_name, f in field_map.iteritems(): 
     211            if not f.assigned and not f.readonly: 
    212212                for arg in args: 
    213213                    if hasattr(arg, field_name): 
     
    216216        # Assign default values for fields that haven't yet been 
    217217        # assigned a value. 
    218         for field in field_map.itervalues(): 
    219             if not field.assigned and not field.readonly: 
    220                 default = field.default[0] 
     218        field_spec = self._field_spec 
     219        for f in field_map.itervalues(): 
     220            if not f.assigned and not f.readonly: 
     221                default = f.default[0] 
     222                if f.may_store_entities and not callable(default): 
     223                    field_name = f._name 
     224                    default = resolve(f._instance._db, field_name, default, 
     225                                      field_spec[field_name]) 
    221226                while callable(default) and default is not UNASSIGNED: 
    222227                    default = default() 
    223                 field.set(default) 
     228                f.set(default) 
    224229 
    225230    def _setup(self): 
     
    615620 
    616621    def _execute(self, db): 
    617         execute = db.execute 
    618         processing = [] 
    619622        data_attr = self._data_attr 
    620         def process_data(extent): 
    621             """Recursively process data, parents before children.""" 
    622             if extent in processing or extent not in self._extents: 
    623                 return 
    624             processing.append(extent) 
    625             # Get the field spec from the extent's create transaction 
    626             # by instantiating a new create transaction. 
    627             create = extent.t.create 
    628             tx = create() 
    629             field_spec = tx._field_spec.copy() 
    630             # Remove readonly fields since we can't set them, and 
    631             # remove hidden fields since we can't "see" them. 
    632             for name in field_spec.keys(): 
    633                 delete = False 
    634                 if not hasattr(tx.f, name): 
    635                     # The create transaction's _setup() might delete a 
    636                     # field without deleting the field_spec entry. 
    637                     delete = True 
    638                 else: 
    639                     f = getattr(tx.f, name) 
    640                 if delete or f.readonly or f.hidden: 
    641                     del field_spec[name] 
    642             field_names = field_spec.keys() 
    643             field_classes = field_spec.values() 
    644             has_entity_field = False 
    645             for FieldClass in field_classes: 
    646                 if issubclass(FieldClass, field.Entity): 
    647                     has_entity_field = True 
    648                     allow = FieldClass.allow 
    649                     for extent_name in allow: 
    650                         parent_extent = db.extent(extent_name) 
    651                         process_data(parent_extent) 
    652             # Get the data we need to process. 
    653             data = [] 
    654             if hasattr(extent._EntityClass, data_attr): 
    655                 data = getattr(extent._EntityClass, data_attr) 
    656             if callable(data): 
    657                 data = data(db) 
    658             if not data: 
    659                 return 
    660             for values in data: 
    661                 value_map = {} 
    662                 for field_name, value, FieldClass in zip( 
    663                     field_names, values, field_classes 
    664                     ): 
    665                     if value is not DEFAULT: 
    666                         value = resolve(field_name, value, FieldClass, 
    667                                         field_names) 
    668                         value_map[field_name] = value 
    669                 new = create(**value_map) 
    670                 try: 
    671                     execute(new) 
    672                 except: 
    673                     print '-' * 40 
    674                     print 'extent:', extent 
    675                     print 'data:', data 
    676                     print 'field_spec:', field_spec 
    677                     print 'value_map:', value_map 
    678                     raise 
    679         def resolve(field_name, value, FieldClass, field_names): 
    680             # Since a callable data might resolve entity fields 
    681             # itself, we only do a lookup here if the value supplied 
    682             # is not an Entity instance. 
    683             if (issubclass(FieldClass, field._EntityBase) 
    684                 and not isinstance(value, base.Entity) 
    685                 and value is not UNASSIGNED 
    686                 ): 
    687                 if isinstance(value, list): 
    688                     value = [ 
    689                         resolve(field_name, v, FieldClass, field_names) 
    690                         for v in value 
    691                         ] 
    692                 elif isinstance(value, set): 
    693                     value = set([ 
    694                         resolve(field_name, v, FieldClass, field_names) 
    695                         for v in value 
    696                         ]) 
    697                 else: 
    698                     allow = FieldClass.allow 
    699                     if len(allow) > 1: 
    700                         # With more than one allow we need to have been 
    701                         # told which extent to use. 
    702                         extent_name, value = value 
    703                     else: 
    704                         # Only one Entity is allowed so we do not expect 
    705                         # the extent name in the data. 
    706                         extent_name = set(allow).pop() 
    707                     lookup_extent = db.extent(extent_name) 
    708                     default_key = lookup_extent.default_key 
    709                     if isinstance(value, dict): 
    710                         kw = value 
    711                     elif isinstance(value, tuple): 
    712                         if len(default_key) != len(value): 
    713                             msg = 'mismatch between default key %r and value %r' 
    714                             raise ValueError, msg % (default_key, value) 
    715                         kw = dict(zip(default_key, value)) 
    716                         for key_field_name in default_key: 
    717                             FClass = lookup_extent.field_spec[key_field_name] 
    718                             v = resolve(key_field_name, kw[key_field_name], 
    719                                         FClass, default_key) 
    720                             kw[key_field_name] = v 
    721                     else: 
    722                         msg = 'value %r is not valid for field %r in %r' % ( 
    723                             value, field_name, field_names) 
    724                         raise TypeError(msg) 
    725                     value = lookup_extent.findone(**kw) 
    726             return value 
    727623        # Main processing loop.  Process extents by highest priority first. 
    728624        priority_attr = self._data_attr + '_priority' 
     
    740636            for extent in extents 
    741637            )) 
     638        # Process data in order of priority. 
     639        processing = [] 
    742640        for priority, extent in priority_extents: 
    743             process_data(extent
     641            self._process_data(db, extent, processing
    744642        # Call module-level handlers. 
    745643        fn = getattr(db._schema_module, 'on' + data_attr, None) 
     
    747645            fn(db) 
    748646 
     647    def _process_data(self, db, extent, processing): 
     648        """Recursively process initial/sample data, parents before children. 
     649 
     650        - `db`: Database to work within. 
     651        - `extent`: Extent to process. 
     652        - `processing`: List of extents that are being processed higher up 
     653          in the call stack. 
     654        """ 
     655        if extent in processing: 
     656            return 
     657        processing.append(extent) 
     658        # Get the data we need to process, and short-circuit if no 
     659        # data is specified. 
     660        data = [] 
     661        data_attr = self._data_attr 
     662        if hasattr(extent._EntityClass, data_attr): 
     663            data = getattr(extent._EntityClass, data_attr) 
     664        if callable(data): 
     665            data = data(db) 
     666        if not data: 
     667            return 
     668        # Get the field spec from the extent's create transaction 
     669        # by instantiating a new create transaction. 
     670        create = extent.t.create 
     671        tx = create() 
     672        field_spec = tx._field_spec.copy() 
     673        # Remove readonly fields since we can't set them, and 
     674        # remove hidden fields since we can't "see" them. 
     675        for name in field_spec.keys(): 
     676            delete = False 
     677            if not hasattr(tx.f, name): 
     678                # The create transaction's _setup() might delete a 
     679                # field without deleting the field_spec entry. 
     680                delete = True 
     681            else: 
     682                f = getattr(tx.f, name) 
     683            if delete or f.readonly or f.hidden: 
     684                del field_spec[name] 
     685        field_names = field_spec.keys() 
     686        field_classes = field_spec.values() 
     687        has_entity_field = False 
     688        for FieldClass in field_classes: 
     689            if issubclass(FieldClass, field.Entity): 
     690                has_entity_field = True 
     691                allow = FieldClass.allow 
     692                for extent_name in allow: 
     693                    parent_extent = db.extent(extent_name) 
     694                    self._process_data(db, parent_extent, processing) 
     695        # Process the data. 
     696        execute = db.execute 
     697        for values in data: 
     698            value_map = {} 
     699            for field_name, value, FieldClass in zip( 
     700                field_names, values, field_classes 
     701                ): 
     702                if value is not DEFAULT: 
     703                    value = resolve(db, field_name, value, FieldClass, 
     704                                    field_names) 
     705                    value_map[field_name] = value 
     706            new = create(**value_map) 
     707            try: 
     708                execute(new) 
     709            except: 
     710                print '-' * 40 
     711                print 'extent:', extent 
     712                print 'data:', data 
     713                print 'field_spec:', field_spec 
     714                print 'value_map:', value_map 
     715                raise 
     716 
    749717 
    750718class Initialize(_Populate): 
     
    789757    def _execute(self, db): 
    790758        return self._fn(db) 
     759 
     760 
     761# --------------------------------------------------------------------- 
     762 
     763 
     764def resolve(db, field_name, value, FieldClass, field_names=None): 
     765    """Resolve the entity reference(s) in `value` and return the 
     766    actual entity references. 
     767 
     768    - `db`: Database to search within. 
     769    - `field_name`: Field name for the field whose value we are 
     770      resolving. 
     771    - `value`: Value to resolve. 
     772    - `FieldClass`: Class of the field whose value we are 
     773      resolving. 
     774    - `field_names`: (optional) Full list of field names for each data 
     775      population record, if resolving within initial or sample data. 
     776      Used to make error messages more useful. 
     777    """ 
     778    # Since a callable data might resolve entity fields 
     779    # itself, we only do a lookup here if the value supplied 
     780    # is not an Entity instance. 
     781    if (issubclass(FieldClass, field._EntityBase) 
     782        and not isinstance(value, base.Entity) 
     783        and value is not UNASSIGNED 
     784        ): 
     785        if isinstance(value, list): 
     786            value = [ 
     787                resolve(db, field_name, v, FieldClass, field_names) 
     788                for v in value 
     789                ] 
     790        elif isinstance(value, set): 
     791            value = set([ 
     792                resolve(db, field_name, v, FieldClass, field_names) 
     793                for v in value 
     794                ]) 
     795        else: 
     796            allow = FieldClass.allow 
     797            if len(allow) > 1: 
     798                # With more than one allow we need to have been 
     799                # told which extent to use. 
     800                extent_name, value = value 
     801            else: 
     802                # Only one Entity is allowed so we do not expect 
     803                # the extent name in the data. 
     804                extent_name = set(allow).pop() 
     805            lookup_extent = db.extent(extent_name) 
     806            default_key = lookup_extent.default_key 
     807            if isinstance(value, dict): 
     808                kw = value 
     809            elif isinstance(value, tuple): 
     810                if len(default_key) != len(value): 
     811                    msg = 'mismatch between default key %r and value %r' 
     812                    raise ValueError, msg % (default_key, value) 
     813                kw = dict(zip(default_key, value)) 
     814                for key_field_name in default_key: 
     815                    FClass = lookup_extent.field_spec[key_field_name] 
     816                    v = resolve(db, key_field_name, kw[key_field_name], 
     817                                FClass, default_key) 
     818                    kw[key_field_name] = v 
     819            else: 
     820                msg = 'value %r is not valid for field %r in %r' % ( 
     821                    value, field_name, field_names) 
     822                raise TypeError(msg) 
     823            value = lookup_extent.findone(**kw) 
     824            if value is None: 
     825                raise ValueError('no entity %s found in %s' % 
     826                                 (kw, lookup_extent)) 
     827    return value 
    791828 
    792829