Schema-by-interface options

Martin Aspeli <[email protected]> Fri, 8 Sep 2006 08:14:53 -0700 (PDT)
Newsgroups gmane.comp.web.zope.plone.archetypes.devel
Message-ID <[email protected]>
Hi guys,

I'm still quite serious about wanting to make AT schemas more adaptable (pun
intended). This is in reference to the basically-working solution I put in
b-org, at http://svn.plone.org/svn/collective/borg/trunk/content/schema.py,
and to the whitmo-schema-by-iface branch of Archetypes.

One question right up front - the whitmo-schema-by-iface branch was created
from the whitmo-iface-ageddon branch. This in turn was copied from 1.4. I'm
planning to look at this for 1.5, but what is the status of the
iface-ageddon branch? Has it been merged? Does it need to be merged?

Secondly, to take a step back, I see two fundamental use cases that I'd like
to support:

 1) Replace the schema of an existing content type (most likely by extending
it or changing parts of it, e.g. plugging in a new widget)
 2) Add a particular schema field to a number of content types (e.g. add a
"tags" field to all or some content types when a tagging solution is
installed)

There is one main challenge in making this work, which any solution will
need to deal with:

 - Various bits of AT depend on there being ClassGen'd methods (getFoo() and
setFoo() for field 'foo') for each field. Those methods get created only
when the class is ClassGen'd. This in turn always checks the class attribute
'schema'. Class attributes cannot be made dynamic in the same way instance
attributes can with @property. Therefore, you must run ClassGen each time
the schema changes. This is the reason why VariableSchemaSupport is so slow,
because it re-calculates on every access (mainly due to a bug/poor design).
In b-org, I use a cache so that it only calls ClassGen the first time
Schema() is called (or when Zope is restarted, it's not persistent), or
after an ISchemaInvalidatedEvent is received.

 - Since this is basically infrastructure that is needed to work around AT
suck, and since it needs to happen exactly once each time a schema is
changed (not each time it's looked up!) I'd suggest that we make the
Schema() method in BaseObject responsible for doing this, in the same way
that Schema() in ExtensibleSchemaSupport in b-org does.

I think it's important to meet both the original use cases with the design.
The schema-by-interface branch is concerned really only with case (1) above
(wholesale replacement of the schema, e.g. by registering a more specific
adapter). The original discussion ended by saying that the adapter from
context -> ISchema (which is what the Schema() method does - in fact, it's
just a lambda self: ISchema(self)) could be replaced by one that knew about
aggregation. That means each type that the hypothetical tagging solution
could plug into would need to explicitly mark their ability to be
schema-extended. At least, I think the default position should be
extensibility, and I think that AT needs to come with the necessary
machinery rather than leave it up to each component to invent it like b-org
did.

I also take some issue with this -
http://dev.plone.org/archetypes/browser/Archetypes/branches/whitmo-schema-by-iface/Schema/adapters.py?rev=6267,
which is the default adapter from IBaseObject to ISchema that is the result
of Schema = labmda self: ISchema(self). This manipulates the __class__ and
__dict__ of the adapter, which seems quite hacky (perhaps consistently with
the rest of AT and ClassGen, but ...)

I could imagine something like this would work, which I think would be quite
easily doable based on the experience I gained from b-org (I could even
adapt the tests....):

 o BaseObject's Schema() takes care of the schema cache like
ExtensibleSchemaSupport in
http://svn.plone.org/svn/collective/borg/trunk/content/schema.py. The
ISchemaInvalidatedEvent etc. works the same way. As far as I can tell, this
approach has negligible performance overhead (after the first schema fetch,
which is slower), and third party code that needs schema changes to be
reflected at run time (e.g. some install script for a new service that
doesn't want to require a restart) can fire this event.

 o Schema() adapts self to ISchemaComposer, which is responsible for finding
schemas and returning a single, aggregated schema. This schema is then
cached (so that the composer is only invoked once unless the event is fired)
and returned in an aq wrapper.

 o The schema composer looks up adapters from the context to ISchemaSource.
The ISchemaSource is a callable that can return a schema. That way, you
would write small adapters that simply had __call__(self): return Schema (
... ) to plug in a new schema. Note that you could, for example, have an
ITaggable marker interface and register the ISchemaSource for ITaggable, so
that only ITaggable-marked objects get the 'tags' field. If you want a
completely global version, you could register an adapter for Interface, and
if you want a site-local (only-when-installed) schema, then you can register
a local adapter (yay for Zope 2.10 - you can even do it with GenericSetup)

 o A default ISchemaSource component would look at klass.schema to find the
schema, so as to provide full backwards compatability support. Simple cases
(i.e. most cases) can keep doing schema = MySchema in the class definition.

The ISchemaComposer, however, needs to know how to put the schemas together,
and in which order. Obviously, if you have specific needs, you could
register a specific ISchemaComposer adapter that may even ignore the
ISchemaSource components (i.e. fi you just wanted a particular schema every
time). However, there must be some generic version. 

One option (similar to how menus work in Zope 3) is to let each
ISchemaSource specify an ordering, which is just an int. The composer could
use that - it looks up all applicable adapters to ISchemaSource, orders them
and then appends the schemas they produce in that order.

An alternative (or additional step), which is what I do in b-org, is to let
the ISchemaSoruce's __call__() method take the current schema as a parameter
so that it can examine it and make decisions. Obviously, not every schema
source will know about every other schema source though.

Other patterns gratefully accepted!

In fact, there are some problems that could arise, for example of two schema
sources define fields with the same name but different types/meanings. AT
has no good way of dealing with that (you'd need namespaces on fields; the
whole ClassGen thing would have to go away, which would mean refactoring a
lot of things that depend on it). I expect there will be a small number of
general schema plug-ins, though, and these would need to be coded quite
defensively. Specific schema sources for specific content types would be
more common, since it gives you a way of doing a per-use-case modification
of a type's schema without subclassing or modifying that type directly. In
either case, I think the potential benefits are quite big.

This is what I'd like to do, anyway - in time for Plone 3.0. It fits in well
with the other generalisability-work we've been doing on the content menu
and with viewlets. And I've already done it in b-org, so I know it can work
and I know where the pitfalls are. I even have tests! :)

Martin
-- 
View this message in context: http://www.nabble.com/Schema-by-interface-options-tf2239709.html#a6210974
Sent from the Archetypes - devel forum at Nabble.com.


-------------------------------------------------------------------------
Using Tomcat but need to do more? Need to support web services, security?
Get stuff done quickly with pre-integrated technology to make your job easier
Download IBM WebSphere Application Server v.1.0.1 based on Apache Geronimo
http://sel.as-us.falkag.net/sel?cmd=lnk&kid=120709&bid=263057&dat=121642