adding support for abstract base classes
This commit is contained in:
@@ -46,6 +46,10 @@ def delete_keyspace(name):
|
||||
execute("DROP KEYSPACE {}".format(name))
|
||||
|
||||
def create_table(model, create_missing_keyspace=True):
|
||||
|
||||
if model.__abstract__:
|
||||
raise CQLEngineException("cannot create table from abstract model")
|
||||
|
||||
#construct query string
|
||||
cf_name = model.column_family_name()
|
||||
raw_cf_name = model.column_family_name(include_keyspace=False)
|
||||
|
||||
@@ -2,7 +2,7 @@ from collections import OrderedDict
|
||||
import re
|
||||
|
||||
from cqlengine import columns
|
||||
from cqlengine.exceptions import ModelException
|
||||
from cqlengine.exceptions import ModelException, CQLEngineException
|
||||
from cqlengine.query import QuerySet, DMLQuery
|
||||
from cqlengine.query import DoesNotExist as _DoesNotExist
|
||||
from cqlengine.query import MultipleObjectsReturned as _MultipleObjectsReturned
|
||||
@@ -29,6 +29,8 @@ class hybrid_classmethod(object):
|
||||
|
||||
class QuerySetDescriptor(object):
|
||||
def __get__(self, obj, model):
|
||||
if model.__abstract__:
|
||||
raise CQLEngineException('cannot execute queries against abstract models')
|
||||
return QuerySet(model)
|
||||
|
||||
class BaseModel(object):
|
||||
@@ -183,6 +185,8 @@ class ModelMetaClass(type):
|
||||
for k,v in getattr(base, '_defined_columns', {}).items():
|
||||
inherited_columns.setdefault(k,v)
|
||||
|
||||
#short circuit __abstract__ inheritance
|
||||
is_abstract = attrs['__abstract__'] = attrs.get('__abstract__', False)
|
||||
|
||||
def _transform_column(col_name, col_obj):
|
||||
column_dict[col_name] = col_obj
|
||||
@@ -208,7 +212,7 @@ class ModelMetaClass(type):
|
||||
defined_columns = OrderedDict(column_definitions)
|
||||
|
||||
#prepend primary key if one hasn't been defined
|
||||
if not any([v.primary_key for k,v in column_definitions]):
|
||||
if not is_abstract and not any([v.primary_key for k,v in column_definitions]):
|
||||
k,v = 'id', columns.UUID(primary_key=True)
|
||||
column_definitions = [(k,v)] + column_definitions
|
||||
|
||||
@@ -228,7 +232,9 @@ class ModelMetaClass(type):
|
||||
clustering_keys = OrderedDict(k for k in primary_keys.items() if not k[1].partition_key)
|
||||
|
||||
#setup partition key shortcut
|
||||
assert partition_keys
|
||||
if len(partition_keys) == 0:
|
||||
if not is_abstract:
|
||||
raise ModelException("at least one partition key must be defined")
|
||||
if len(partition_keys) == 1:
|
||||
pk_name = partition_keys.keys()[0]
|
||||
attrs['pk'] = attrs[pk_name]
|
||||
@@ -294,6 +300,7 @@ class Model(BaseModel):
|
||||
the db name for the column family can be set as the attribute db_name, or
|
||||
it will be genertaed from the class name
|
||||
"""
|
||||
__abstract__ = True
|
||||
__metaclass__ = ModelMetaClass
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from cqlengine.query import QueryException
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
|
||||
from cqlengine.exceptions import ModelException
|
||||
from cqlengine.exceptions import ModelException, CQLEngineException
|
||||
from cqlengine.models import Model
|
||||
from cqlengine import columns
|
||||
import cqlengine
|
||||
@@ -199,8 +200,79 @@ class TestManualTableNaming(BaseCassEngTestCase):
|
||||
class InheritedTest(self.RenamedTest): pass
|
||||
assert InheritedTest.table_name is None
|
||||
|
||||
|
||||
|
||||
class AbstractModel(Model):
|
||||
__abstract__ = True
|
||||
|
||||
class ConcreteModel(AbstractModel):
|
||||
pkey = columns.Integer(primary_key=True)
|
||||
data = columns.Integer()
|
||||
|
||||
class AbstractModelWithCol(Model):
|
||||
__abstract__ = True
|
||||
pkey = columns.Integer(primary_key=True)
|
||||
|
||||
class ConcreteModelWithCol(AbstractModelWithCol):
|
||||
data = columns.Integer()
|
||||
|
||||
class AbstractModelWithFullCols(Model):
|
||||
__abstract__ = True
|
||||
pkey = columns.Integer(primary_key=True)
|
||||
data = columns.Integer()
|
||||
|
||||
class TestAbstractModelClasses(BaseCassEngTestCase):
|
||||
|
||||
def test_id_field_is_not_created(self):
|
||||
""" Tests that an id field is not automatically generated on abstract classes """
|
||||
assert not hasattr(AbstractModel, 'id')
|
||||
assert not hasattr(AbstractModelWithCol, 'id')
|
||||
|
||||
def test_id_field_is_not_created_on_subclass(self):
|
||||
assert not hasattr(ConcreteModel, 'id')
|
||||
|
||||
def test_abstract_attribute_is_not_inherited(self):
|
||||
""" Tests that __abstract__ attribute is not inherited """
|
||||
assert not ConcreteModel.__abstract__
|
||||
assert not ConcreteModelWithCol.__abstract__
|
||||
|
||||
def test_attempting_to_save_abstract_model_fails(self):
|
||||
""" Attempting to save a model from an abstract model should fail """
|
||||
with self.assertRaises(CQLEngineException):
|
||||
AbstractModelWithFullCols.create(pkey=1, data=2)
|
||||
|
||||
def test_attempting_to_create_abstract_table_fails(self):
|
||||
""" Attempting to create a table from an abstract model should fail """
|
||||
from cqlengine.management import create_table
|
||||
with self.assertRaises(CQLEngineException):
|
||||
create_table(AbstractModelWithFullCols)
|
||||
|
||||
def test_attempting_query_on_abstract_model_fails(self):
|
||||
""" Tests attempting to execute query with an abstract model fails """
|
||||
with self.assertRaises(CQLEngineException):
|
||||
iter(AbstractModelWithFullCols.objects(pkey=5)).next()
|
||||
|
||||
def test_abstract_columns_are_inherited(self):
|
||||
""" Tests that columns defined in the abstract class are inherited into the concrete class """
|
||||
assert hasattr(ConcreteModelWithCol, 'pkey')
|
||||
assert isinstance(ConcreteModelWithCol.pkey, property)
|
||||
assert isinstance(ConcreteModelWithCol._columns['pkey'], columns.Column)
|
||||
|
||||
def test_concrete_class_table_creation_cycle(self):
|
||||
""" Tests that models with inherited abstract classes can be created, and have io performed """
|
||||
from cqlengine.management import create_table, delete_table
|
||||
create_table(ConcreteModelWithCol)
|
||||
|
||||
w1 = ConcreteModelWithCol.create(pkey=5, data=6)
|
||||
w2 = ConcreteModelWithCol.create(pkey=6, data=7)
|
||||
|
||||
r1 = ConcreteModelWithCol.get(pkey=5)
|
||||
r2 = ConcreteModelWithCol.get(pkey=6)
|
||||
|
||||
assert w1.pkey == r1.pkey
|
||||
assert w1.data == r1.data
|
||||
assert w2.pkey == r2.pkey
|
||||
assert w2.data == r2.data
|
||||
|
||||
delete_table(ConcreteModelWithCol)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -128,6 +128,10 @@ Model Methods
|
||||
Model Attributes
|
||||
================
|
||||
|
||||
.. attribute:: Model.__abstract__
|
||||
|
||||
*Optional.* Indicates that this model is only intended to be used as a base class for other models. You can't create tables for abstract models, but checks around schema validity are skipped during class construction.
|
||||
|
||||
.. attribute:: Model.table_name
|
||||
|
||||
*Optional.* Sets the name of the CQL table for this model. If left blank, the table name will be the name of the model, with it's module name as it's prefix. Manually defined table names are not inherited.
|
||||
|
||||
Reference in New Issue
Block a user