Friday, May 29, 2009

sqaencode 0.1

I have been messing around a bit and now pretty much have a working lib for encoding nested models into forms and back and automatically generating formencode Schema from sqlalchemy models.

E:\python_repos\sqaencode>nosetests --with-coverage --cover-package=sqaencode
................
Name Stmts Exec Cover Missing
----------------------------------------------------
sqaencode 6 6 100%
sqaencode.constants 10 10 100%
sqaencode.decode 47 45 95% 126, 161
sqaencode.encode 28 26 92% 90-91
sqaencode.util 12 12 100%
sqaencode.validators 46 42 91% 30, 64, 66, 129
----------------------------------------------------
TOTAL 149 141 94%
----------------------------------------------------------------------
Ran 16 tests in 0.962s

OK

I created a ModelSchema class, subclassing formencode.Schema and giving it an inline __metaclass__ inheriting from formencode.declarative.DeclarativeMeta. I over-rode the __repr__ to output an (almost) eval()able representation.

class ModelSchema(Schema):
class __metaclass__(DeclarativeMeta):
def __repr__(cls):
model = cls.__model__.__name__
base = [ "class %(model)sSchema(model_schema(%(model)s)):" % dict (
model = model) ]

for arg in SCHEMA_ARGS:
base.append(' %-20s = %r' % (arg, getattr(cls, arg)))

base.append('')
for key, validator in sorted(cls.fields.items()):
if key.startswith('_'): continue

args = non_default_validator_args(validator)
base.append(' %-20s = %s(%s)' % (
key,
type(validator).__name__, args) )

return '\n'.join(base)

Using the model_schema factory to create a ModelSchema and then getting a repr:
In [1]: from sqaencode import model_schema

In [2]: model_schema(Product)
Out[2]:
class ProductSchema(model_schema(Product)):
ignore_key_missing = True
allow_extra_fields = True
pre_validators = []

active = Bool()
amount = UnicodeString()
colour = UnicodeString()
description = UnicodeString()
featured = Bool()
id = Int()
image = UnicodeString()
image_thumb = UnicodeString()
image_zoom = UnicodeString()
keywords = UnicodeString()
material = UnicodeString()
name = UnicodeString()
ordernum = Int()
sku = UnicodeString()
views = Int()

The __metaclass__ __repr__ hack serves a dual purpose. a) for debugging and b) as templating. By printing the repr you can use that as a template for customizing a model schema. You actually inherit from a dynamically generated class ( a function call taking a model and optional arguments). I wasn't even too sure you could do that in python. It's nice I don't have to create a new meta class mechanism and can stick with existing formencode semantics.

Note the `ignore_key_missing` flag that is by default set to True. I think when using this I will just define which fields to validate purely by virtue of what is included in the html. eg If there is no `sku` field in the form then it will not be validated.

What if I *didn't* want to globally ignore missing keys and wanted to manually declare which to ignore? Formencode has an in-built sub-classing mechanism whereby if you declare `some_key = None` then some_key will not be validated at all.

To declare a Product model_schema with plural Colors inline:
class ProductSchema(model_schema(Product, nested=True)):
colors = model_schema(Color, plural=True)

With Python 2.6 you can do inline customisable declarations of relations:
class ProductSchema(model_schema(Product, nested=True)):
@sqaencode.plural
class colors(model_schema(Color)):
some_field = NonDefault()


I'm thinking about creating a mechanism whereby I subclass sqlalchemy.types.* for metadata purposes to further drive automatic schema generation. A cool thing about Django's tight integration is the high level data types. Url, Email etc You declare higher level properties to what are essentially stored as VARCHAR types in the database. It's not *just* a string.

SqlAlchemy, while really great, (rightly) doesn't try and abstract beyond basic SQL. There is nothing stopping an end user however doing something like this:
1  class Url(Unicode):   pass
2 class Email(Unicode): pass
3
4 higher_level_table = Table ( 'higher_level', metadata,
5 Column(
'url', Url(32)),
6 Column(
'email', Email(32)),
7 Column(
'id', Integer(), primary_key=True, autoincrement=True, nullable=False),
8 )
9

If Url and Email were imported column types from sqaencode.types then you could add them to the sqalchemy => formencode type mapping and they would be picked up by model_schema()

What's on the todo?
  • Options for automatically generating child Schemas.
  • Setting the length on String/UnicodeString validators automatically to the max length of the corresponding column.
  • Automatically create unique column validators

No comments: