Changeset 3550
- Timestamp:
- 09/12/07 16:53:26 (1 year ago)
- Files:
-
- trunk/Schevo/doc/SchevoSchemaDefinition.txt (modified) (5 diffs)
- trunk/Schevo/schevo/test/test_field_entity.py (modified) (3 diffs)
- trunk/Schevo/schevo/test/test_field_entitylist.py (modified) (3 diffs)
- trunk/Schevo/schevo/test/test_field_entityset.py (modified) (2 diffs)
- trunk/Schevo/schevo/transaction.py (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/Schevo/doc/SchevoSchemaDefinition.txt
r3258 r3550 7 7 8 8 .. contents:: 9 10 .. Quick reference: Order of section delineators is === --- ... ~~~ 9 11 10 12 … … 326 328 ------------------- 327 329 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:: 330 Initial data is data that Schevo uses when creating a new database. 331 Initial data is *always* processed during new database creation. 332 Specify initial data for an extent by assigning an `_initial` 333 attribute to the entity class in your database schema. 334 335 Sample data is data that Schevo uses to populate a database after its 336 creation. Sample data is only processed if the ``-p`` or ``--sample`` 337 option is passed to the ``schevo db create`` command-line tool, or if 338 you call the `populate` method on an open database. Specify sample 339 data for an extent by assigning a `_sample` attribute to the entity 340 class in your database schema. 341 342 Unit test sample data is data that Schevo populates a database with 343 when running a suite of unit tests against a database schema. Specify 344 unit test sample data for an extent by assigning a 345 `_sample_unittest` attribute to the entity class in your database 346 schema. 347 348 You may also specify sample data with a custom attribute name, such as 349 `_sample_abc`. Populate a database with your custom sample data by 350 passing the suffix to the call to `populate` on your database. 351 For the example, to populate `db` with the `abc` sample data, call 352 ``db.populate('abc')``. 353 354 Specify each collection of initial or sample data as a list of tuples, 355 where each tuple contains values for the fields that the extent's 356 `create` transaction expects, in the order that it expects 357 them. Normally, those fields are the same as the fields specified in 358 the entity class, but in complex database schemata the fields may 359 differ. 360 361 To 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``:: 330 364 331 365 class Foo(E.Entity): 332 366 333 367 name = f.string() 368 size = f.integer(default=5) 334 369 335 370 _key(name) 336 371 337 372 _initial = [ 338 (' One',),339 (' Two',),340 (' Buckle',),341 (' Shoe',),373 ('a', 1), 374 ('b', 2), 375 ('c', DEFAULT), 376 ('d', DEFAULT), 342 377 ] 343 378 344 class FooChild(E.Entity): 379 To specify a value for an `Entity` field that allows only one entity 380 type, use a tuple containing the values of the fields of the first key 381 of 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): 345 399 346 400 foo = f.entity('Foo') 347 ba r= f.string()401 baz = f.string() 348 402 349 403 _key(foo, bar) 350 404 351 405 _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.'), 356 410 ] 357 411 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. 412 To specify a value for an `Entity` field that allows more than one 413 entity type, use a two-tuple that first gives the entity type, then 414 gives a tuple of the values of the fields of the first key of that 415 entity 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 451 To specify values for `EntityList` and `EntitySet` fields, simply 452 enclose the representations of entities within a list or a set, 453 respectively. 361 454 362 455 … … 380 473 birthdate = f.date(required=False) # [2] 381 474 disposition = f.entity('Disposition', on_delete=UNASSIGN, 382 required=False) # [3] 475 required=False, 476 default=('Cheerful',)) # [3] 383 477 384 478 1. The `name` field is a required `Unicode` field. By default, all … … 388 482 389 483 3. 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. 393 492 394 493 … … 412 511 safest operation, since it prevents accidental deletion of an entity 413 512 if it is being referred to by another entity. 513 514 515 Default field values 516 .................... 517 518 Default field values are assigned to fields in `Create` transactions. 519 520 Specify default field values in a field definition by giving either a 521 default value in the same notation as you would for initial or sample 522 data, or by specifying a callable that returns the actual value to be 523 used as the default value. 524 525 Default values are not assigned to a field if a value has been 526 supplied for that field in the keyword arguments specified when 527 creating the `Create` transaction. 414 528 415 529 trunk/Schevo/schevo/test/test_field_entity.py
r3513 r3550 18 18 19 19 thing = f.string() 20 21 _key(thing) 20 22 21 23 _sample_unittest = [ … … 39 41 class Baz(E.Entity): 40 42 41 foo = f.entity('Foo' )43 foo = f.entity('Foo', default=('a', )) 42 44 bar = f.entity('Bar', default=default_bar) 43 45 foobar = f.entity('Foo', 'Bar', required=False) … … 55 57 assert f.convert('Foo-1', db) == db.Foo[1] 56 58 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) 57 64 58 65 def test_reversible_valid_values(self): trunk/Schevo/schevo/test/test_field_entitylist.py
r3519 r3550 1 2 1 """EntityList field unit tests. 3 2 … … 79 78 80 79 _initial_priority = 1 80 81 82 class BooBoo(E.Entity): 83 84 foo_foos = f.entity_list('FooFoo', 85 default=[('foofoo2', ), ('foofoo1', )]) 81 86 82 87 … … 124 129 ] 125 130 ''' 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')] 126 136 127 137 def test_store_and_retrieve_UNASSIGNED(self): trunk/Schevo/schevo/test/test_field_entityset.py
r3519 r3550 60 60 61 61 _initial_priority = 1 62 63 64 class BooBoo(E.Entity): 65 66 foo_foos = f.entity_set('FooFoo', 67 default=set([('foofoo2', ), ('foofoo1', )])) 62 68 63 69 … … 105 111 ] 106 112 ''' 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')]) 107 118 108 119 def test_store_and_retrieve_UNASSIGNED(self): trunk/Schevo/schevo/transaction.py
r3519 r3550 208 208 setattr(self, name, value) 209 209 # Look for matching field values in objects passed as args. 210 for field_name, f ieldin field_map.iteritems():211 if not f ield.assigned and not field.readonly:210 for field_name, f in field_map.iteritems(): 211 if not f.assigned and not f.readonly: 212 212 for arg in args: 213 213 if hasattr(arg, field_name): … … 216 216 # Assign default values for fields that haven't yet been 217 217 # 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]) 221 226 while callable(default) and default is not UNASSIGNED: 222 227 default = default() 223 f ield.set(default)228 f.set(default) 224 229 225 230 def _setup(self): … … 615 620 616 621 def _execute(self, db): 617 execute = db.execute618 processing = []619 622 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 return624 processing.append(extent)625 # Get the field spec from the extent's create transaction626 # by instantiating a new create transaction.627 create = extent.t.create628 tx = create()629 field_spec = tx._field_spec.copy()630 # Remove readonly fields since we can't set them, and631 # remove hidden fields since we can't "see" them.632 for name in field_spec.keys():633 delete = False634 if not hasattr(tx.f, name):635 # The create transaction's _setup() might delete a636 # field without deleting the field_spec entry.637 delete = True638 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 = False645 for FieldClass in field_classes:646 if issubclass(FieldClass, field.Entity):647 has_entity_field = True648 allow = FieldClass.allow649 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 return660 for values in data:661 value_map = {}662 for field_name, value, FieldClass in zip(663 field_names, values, field_classes664 ):665 if value is not DEFAULT:666 value = resolve(field_name, value, FieldClass,667 field_names)668 value_map[field_name] = value669 new = create(**value_map)670 try:671 execute(new)672 except:673 print '-' * 40674 print 'extent:', extent675 print 'data:', data676 print 'field_spec:', field_spec677 print 'value_map:', value_map678 raise679 def resolve(field_name, value, FieldClass, field_names):680 # Since a callable data might resolve entity fields681 # itself, we only do a lookup here if the value supplied682 # is not an Entity instance.683 if (issubclass(FieldClass, field._EntityBase)684 and not isinstance(value, base.Entity)685 and value is not UNASSIGNED686 ):687 if isinstance(value, list):688 value = [689 resolve(field_name, v, FieldClass, field_names)690 for v in value691 ]692 elif isinstance(value, set):693 value = set([694 resolve(field_name, v, FieldClass, field_names)695 for v in value696 ])697 else:698 allow = FieldClass.allow699 if len(allow) > 1:700 # With more than one allow we need to have been701 # told which extent to use.702 extent_name, value = value703 else:704 # Only one Entity is allowed so we do not expect705 # 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_key709 if isinstance(value, dict):710 kw = value711 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] = v721 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 value727 623 # Main processing loop. Process extents by highest priority first. 728 624 priority_attr = self._data_attr + '_priority' … … 740 636 for extent in extents 741 637 )) 638 # Process data in order of priority. 639 processing = [] 742 640 for priority, extent in priority_extents: 743 process_data(extent)641 self._process_data(db, extent, processing) 744 642 # Call module-level handlers. 745 643 fn = getattr(db._schema_module, 'on' + data_attr, None) … … 747 645 fn(db) 748 646 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 749 717 750 718 class Initialize(_Populate): … … 789 757 def _execute(self, db): 790 758 return self._fn(db) 759 760 761 # --------------------------------------------------------------------- 762 763 764 def 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 791 828 792 829
