thirdparty/google_appengine/google/appengine/ext/db/polymodel.py
changeset 828 f5fd65cc3bf3
child 2864 2e0b0af889be
--- /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 <entity>' 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