diff -r 88c186556a80 -r f5fd65cc3bf3 thirdparty/google_appengine/google/appengine/ext/db/polymodel.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/thirdparty/google_appengine/google/appengine/ext/db/polymodel.py Tue Jan 20 13:19:45 2009 +0000 @@ -0,0 +1,350 @@ +#!/usr/bin/env python +# +# Copyright 2007 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Support for polymorphic models and queries. + +The Model class on its own is only able to support functional polymorphism. +It is possible to create a subclass of Model and then subclass that one as +many generations as necessary and those classes will share all the same +properties and behaviors. The problem is that subclassing Model in this way +places each subclass in their own Kind. This means that it is not possible +to do polymorphic queries. Building a query on a base class will only return +instances of that class from the Datastore, while queries on a subclass will +only return those instances. + +This module allows applications to specify class hierarchies that support +polymorphic queries. +""" + + +from google.appengine.ext import db + +_class_map = {} + +_CLASS_KEY_PROPERTY = 'class' + + +class _ClassKeyProperty(db.ListProperty): + """Property representing class-key property of a polymorphic class. + + The class key is a list of strings describing an polymorphic instances + place within its class hierarchy. This property is automatically calculated. + For example: + + class Foo(PolyModel): ... + class Bar(Foo): ... + class Baz(Bar): ... + + Foo.class_key() == ['Foo'] + Bar.class_key() == ['Foo', 'Bar'] + Baz.class_key() == ['Foo', 'Bar', 'Baz'] + """ + + def __init__(self, name): + super(_ClassKeyProperty, self).__init__(name=name, + item_type=str, + default=None) + + def __set__(self, *args): + raise db.DerivedPropertyError( + 'Class-key is a derived property and cannot be set.') + + def __get__(self, model_instance, model_class): + if model_instance is None: + return self + return [cls.__name__ for cls in model_class.__class_hierarchy__] + + +class PolymorphicClass(db.PropertiedClass): + """Meta-class for initializing PolymorphicClasses. + + This class extends PropertiedClass to add a few static attributes to + new polymorphic classes necessary for their correct functioning. + + """ + + def __init__(cls, name, bases, dct): + """Initializes a class that belongs to a polymorphic hierarchy. + + This method configures a few built-in attributes of polymorphic + models: + + __root_class__: If the new class is a root class, __root_class__ is set to + itself so that it subclasses can quickly know what the root of + their hierarchy is and what kind they are stored in. + __class_hierarchy__: List of classes describing the new model's place + in the class hierarchy. The first element is always the root + element while the last element is the new class itself. For example: + + class Foo(PolymorphicClass): ... + + class Bar(Foo): ... + + class Baz(Bar): ... + + Foo.__class_hierarchy__ == [Foo] + Bar.__class_hierarchy__ == [Foo, Bar] + Baz.__class_hierarchy__ == [Foo, Bar, Baz] + + Unless the class is a root class or PolyModel itself, it is not + inserted in to the kind-map like other models. However, all polymorphic + classes, are inserted in to the class-map which maps the class-key to + implementation. This class key is consulted using the polymorphic instances + discriminator (the 'class' property of the entity) when loading from the + datastore. + """ + if name == 'PolyModel' or PolyModel not in bases: + db._initialize_properties(cls, name, bases, dct) + super(db.PropertiedClass, cls).__init__(name, bases, dct) + else: + cls.__root_class__ = cls + super(PolymorphicClass, cls).__init__(name, bases, dct) + + if name == 'PolyModel': + return + + if cls is not cls.__root_class__: + poly_class = None + for base in cls.__bases__: + if issubclass(base, PolyModel): + poly_class = base + break + else: + raise db.ConfigurationError( + "Polymorphic class '%s' does not inherit from PolyModel." + % cls.__name__) + + cls.__class_hierarchy__ = poly_class.__class_hierarchy__ + [cls] + else: + cls.__class_hierarchy__ = [cls] + + _class_map[cls.class_key()] = cls + + +class PolyModel(db.Model): + """Base-class for models that supports polymorphic queries. + + Use this class to build hierarchies that can be queried based + on their types. + + Example: + + consider the following model hierarchy: + + +------+ + |Animal| + +------+ + | + +-----------------+ + | | + +------+ +------+ + |Canine| |Feline| + +------+ +------+ + | | + +-------+ +-------+ + | | | | + +---+ +----+ +---+ +-------+ + |Dog| |Wolf| |Cat| |Panther| + +---+ +----+ +---+ +-------+ + + This class hierarchy has three levels. The first is the "root class". + All models in a single class hierarchy must inherit from this root. All + models in the hierarchy are stored as the same kind as the root class. + For example, Panther entities when stored to the datastore are of the kind + 'Animal'. Querying against the Animal kind will retrieve Cats, Dogs and + Canines, for example, that match your query. Different classes stored + in the root class' kind are identified by their class-key. When loaded + from the datastore, it is mapped to the appropriate implementation class. + + Polymorphic properties: + + Properties that are defined in a given base-class within a hierarchy are + stored in the datastore for all sub-casses only. So, if the Feline class + had a property called 'whiskers', the Cat and Panther enties would also + have whiskers, but not Animal, Canine, Dog or Wolf. + + Polymorphic queries: + + When written to the datastore, all polymorphic objects automatically have + a property called 'class' that you can query against. Using this property + it is possible to easily write a GQL query against any sub-hierarchy. For + example, to fetch only Canine objects, including all Dogs and Wolves: + + db.GqlQuery("SELECT * FROM Animal WHERE class='Canine'") + + And alternate method is to use the 'all' or 'gql' methods of the Canine + class: + + Canine.all() + Canine.gql('') + + The 'class' property is not meant to be used by your code other than + for queries. Since it is supposed to represents the real Python class + it is intended to be hidden from view. + + Root class: + + The root class is the class from which all other classes of the hierarchy + inherits from. Each hierarchy has a single root class. A class is a + root class if it is an immediate child of PolyModel. The subclasses of + the root class are all the same kind as the root class. In other words: + + Animal.kind() == Feline.kind() == Panther.kind() == 'Animal' + """ + + __metaclass__ = PolymorphicClass + + _class = _ClassKeyProperty(name=_CLASS_KEY_PROPERTY) + + def __new__(cls, *args, **kwds): + """Prevents direct instantiation of PolyModel.""" + if cls is PolyModel: + raise NotImplementedError() + return super(PolyModel, cls).__new__(cls, *args, **kwds) + + @classmethod + def kind(cls): + """Get kind of polymorphic model. + + Overridden so that all subclasses of root classes are the same kind + as the root. + + Returns: + Kind of entity to write to datastore. + """ + if cls is cls.__root_class__: + return super(PolyModel, cls).kind() + else: + return cls.__root_class__.kind() + + @classmethod + def class_key(cls): + """Caclulate the class-key for this class. + + Returns: + Class key for class. By default this is a the list of classes + of the hierarchy, starting with the root class and walking its way + down to cls. + """ + if not hasattr(cls, '__class_hierarchy__'): + raise NotImplementedError( + 'Cannot determine class key without class hierarchy') + return tuple(cls.class_name() for cls in cls.__class_hierarchy__) + + @classmethod + def class_name(cls): + """Calculate class name for this class. + + Returns name to use for each classes element within its class-key. Used + to discriminate between different classes within a class hierarchy's + Datastore kind. + + The presence of this method allows developers to use a different class + name in the datastore from what is used in Python code. This is useful, + for example, for renaming classes without having to migrate instances + already written to the datastore. For example, to rename a polymorphic + class Contact to SimpleContact, you could convert: + + # Class key is ['Information'] + class Information(PolyModel): ... + + # Class key is ['Information', 'Contact'] + class Contact(Information): ... + + to: + + # Class key is still ['Information', 'Contact'] + class SimpleContact(Information): + ... + @classmethod + def class_name(cls): + return 'Contact' + + # Class key is ['Information', 'Contact', 'ExtendedContact'] + class ExtendedContact(SimpleContact): ... + + This would ensure that all objects written previously using the old class + name would still be loaded. + + Returns: + Name of this class. + """ + return cls.__name__ + + @classmethod + def from_entity(cls, entity): + """Load from entity to class based on discriminator. + + Rather than instantiating a new Model instance based on the kind + mapping, this creates an instance of the correct model class based + on the entities class-key. + + Args: + entity: Entity loaded directly from datastore. + + Raises: + KindError when there is no class mapping based on discriminator. + """ + if (_CLASS_KEY_PROPERTY in entity and + tuple(entity[_CLASS_KEY_PROPERTY]) != cls.class_key()): + key = tuple(entity[_CLASS_KEY_PROPERTY]) + try: + poly_class = _class_map[key] + except KeyError: + raise db.KindError('No implementation for class \'%s\'' % key) + return poly_class.from_entity(entity) + return super(PolyModel, cls).from_entity(entity) + + @classmethod + def all(cls): + """Get all instance of a class hierarchy. + + Returns: + Query with filter set to match this class' discriminator. + """ + query = super(PolyModel, cls).all() + if cls != cls.__root_class__: + query.filter(_CLASS_KEY_PROPERTY + ' =', cls.class_name()) + return query + + @classmethod + def gql(cls, query_string, *args, **kwds): + """Returns a polymorphic query using GQL query string. + + This query is polymorphic in that it has its filters configured in a way + to retrieve instances of the model or an instance of a subclass of the + model. + + Args: + query_string: properly formatted GQL query string with the + 'SELECT * FROM ' part omitted + *args: rest of the positional arguments used to bind numeric references + in the query. + **kwds: dictionary-based arguments (for named parameters). + """ + if cls == cls.__root_class__: + return super(PolyModel, cls).gql(query_string, *args, **kwds) + else: + from google.appengine.ext import gql + + query = db.GqlQuery('SELECT * FROM %s %s' % (cls.kind(), query_string)) + + query_filter = [('nop', + [gql.Literal(cls.class_name())])] + query._proto_query.filters()[('class', '=')] = query_filter + query.bind(*args, **kwds) + return query