diff --git a/buildout.cfg b/buildout.cfg index 549b7d51..5dcf0c90 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -9,16 +9,18 @@ parts = test scripts sphinx +[tox] +recipe = zc.recipe.egg:script +eggs = tox + crate [test,sqlalchemy,django] + setuptools + [scripts] recipe = zc.recipe.egg:script interpreter = py eggs = crate wheel -[tox] -recipe = zc.recipe.egg:script -eggs = tox - [crate] recipe = hexagonit.recipe.download url = https://cdn.crate.io/downloads/releases/crate-${versions:crate_server}.tar.gz diff --git a/setup.py b/setup.py index ea42b217..7f7c6f05 100644 --- a/setup.py +++ b/setup.py @@ -75,7 +75,8 @@ def read(path): 'mock>=1.0.1', 'zope.testing', 'zc.customdoctests>=1.0.1'], - sqlalchemy=['sqlalchemy>=0.8.2'] + sqlalchemy=['sqlalchemy>=0.8.2'], + django=['django>=1.6,<1.7'] ), install_requires=requirements, package_data={'': ['*.txt']}, diff --git a/src/crate/client/connection.py b/src/crate/client/connection.py index bb67e362..4a807c36 100644 --- a/src/crate/client/connection.py +++ b/src/crate/client/connection.py @@ -59,6 +59,13 @@ def commit(self): if self._closed: raise ProgrammingError("Connection closed") + def rollback(self): + """ + Transactions are not supported, so ``rollback`` is not implemented. + """ + if self._closed: + raise ProgrammingError("Connection closed") + def get_blob_container(self, container_name): """ Retrieve a BlobContainer for `container_name` diff --git a/src/crate/client/django/__init__.py b/src/crate/client/django/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/src/crate/client/django/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/src/crate/client/django/backend/__init__.py b/src/crate/client/django/backend/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/src/crate/client/django/backend/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/src/crate/client/django/backend/base.py b/src/crate/client/django/backend/base.py new file mode 100644 index 00000000..cea00fbc --- /dev/null +++ b/src/crate/client/django/backend/base.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- +from crate.client.connection import Connection +import crate.client.exceptions as Database +from django.db.backends import ( + BaseDatabaseFeatures, + BaseDatabaseWrapper +) + +from .client import DatabaseClient +from .operations import DatabaseOperations +from .creation import DatabaseCreation +from .introspection import DatabaseIntrospection +from .validation import DatabaseValidation + + +class DatabaseFeatures(BaseDatabaseFeatures): + # Does the backend distinguish between '' and None? + interprets_empty_strings_as_nulls = False + + allows_group_by_pk = True + # True if django.db.backend.utils.typecast_timestamp is used on values + # returned from dates() calls. + needs_datetime_string_cast = False + update_can_self_select = True + + can_use_chunked_reads = False + can_return_id_from_insert = False + has_bulk_insert = False + uses_savepoints = False + can_combine_inserts_with_and_without_auto_increment_pk = False + + # If True, don't use integer foreign keys referring to, e.g., positive + # integer primary keys. + related_fields_match_type = False + allow_sliced_subqueries = False + has_select_for_update = False + has_select_for_update_nowait = False + + supports_select_related = False + + # Does the default test database allow multiple connections? + # Usually an indication that the test database is in-memory + test_db_allows_multiple_connections = True + + # Can an object be saved without an explicit primary key? + supports_unspecified_pk = False + + # Can a fixture contain forward references? i.e., are + # FK constraints checked at the end of transaction, or + # at the end of each save operation? + supports_forward_references = False + + # Does a dirty transaction need to be rolled back + # before the cursor can be used again? + requires_rollback_on_dirty_transaction = False + + # Does the backend allow very long model names without error? + supports_long_model_names = True + + # Is there a REAL datatype in addition to floats/doubles? + has_real_datatype = False + supports_subqueries_in_group_by = False + supports_bitwise_or = False + + # Do time/datetime fields have microsecond precision? + supports_microsecond_precision = True + + # Does the __regex lookup support backreferencing and grouping? + supports_regex_backreferencing = False + + # Can date/datetime lookups be performed using a string? + supports_date_lookup_using_string = True + + # Can datetimes with timezones be used? + supports_timezones = False + + # Does the database have a copy of the zoneinfo database? + has_zoneinfo_database = False + + # When performing a GROUP BY, is an ORDER BY NULL required + # to remove any ordering? + requires_explicit_null_ordering_when_grouping = False + + # Can an object have a primary key of 0? MySQL says No. + allows_primary_key_0 = True + + # Do we need to NULL a ForeignKey out, or can the constraint check be + # deferred + can_defer_constraint_checks = False + + # date_interval_sql can properly handle mixed Date/DateTime fields and timedeltas + supports_mixed_date_datetime_comparisons = True + + # Does the backend support tablespaces? Default to False because it isn't + # in the SQL standard. + supports_tablespaces = False + + # Does the backend reset sequences between tests? + supports_sequence_reset = False + + # Confirm support for introspected foreign keys + # Every database can do this reliably, except MySQL, + # which can't do it for MyISAM tables + can_introspect_foreign_keys = False + + # Support for the DISTINCT ON clause + can_distinct_on_fields = False + + # Does the backend decide to commit before SAVEPOINT statements + # when autocommit is disabled? http://bugs.python.org/issue8145#msg109965 + autocommits_when_autocommit_is_off = False + + # Does the backend prevent running SQL queries in broken transactions? + atomic_transactions = False + + # Does the backend support 'pyformat' style ("... %(name)s ...", {'name': value}) + # parameter passing? Note this can be provided by the backend even if not + # supported by the Python driver + supports_paramstyle_pyformat = False + + +class DatabaseWrapper(BaseDatabaseWrapper): + + vendor = 'crate' + operators = { + 'exact': '= %s', + 'iexact': '= %s', + 'contains': 'LIKE %s', + 'icontains': 'LIKE %s', + 'regex': '%s', + 'iregex': '%s', + 'gt': '> %s', + 'gte': '>= %s', + 'lt': '< %s', + 'lte': '<= %s', + 'startswith': 'LIKE %s', + 'endswith': 'LIKE %s', + 'istartswith': 'LIKE %s', + 'iendswith': 'LIKE %s', + } + + Database = Database + + def __init__(self, *args, **kwargs): + super(DatabaseWrapper, self).__init__(*args, **kwargs) + + self.features = DatabaseFeatures(self) + self.ops = DatabaseOperations(self) + self.client = DatabaseClient(self) + self.creation = DatabaseCreation(self) + self.introspection = DatabaseIntrospection(self) + self.validation = DatabaseValidation(self) + + ### CREATING CONNECTIONS AND CURSORS + + def get_connection_params(self): + """Returns a dict of parameters suitable for get_new_connection.""" + servers = self.settings_dict.get("SERVERS", ["localhost:4200"]) + timeout = self.settings_dict.get("TIMEOUT", None) + return { + "servers": servers, + "timeout": timeout + } + + def get_new_connection(self, conn_params): + """Opens a connection to the database.""" + return Connection(**conn_params) + + def init_connection_state(self): + """Initializes the database connection settings.""" + pass + + def create_cursor(self): + """Creates a cursor. Assumes that a connection is established.""" + return self.connection.cursor() + + ### COMMIT + def _commit(self): + pass + # TODO: refresh? + # if self.connection is not None: + # with self.wrap_database_errors: + # self.connection.client. + + ### SAVEPOINT STUFF NOT SUPPORTED + + def _savepoint(self, sid): + pass + + def _savepoint_rollback(self, sid): + pass + + def _savepoint_commit(self, sid): + pass + + def _savepoint_allowed(self): + return False + + ### AUTOCOMMIT NOT SUPPORTED + + def _set_autocommit(self, autocommit): + pass + + ### TEST IF CONNECTION IS USABLE + + def is_usable(self): + """check if connection works""" + try: + self.connection.client._json_request("GET", "/") + except: + return False + else: + return True diff --git a/src/crate/client/django/backend/client.py b/src/crate/client/django/backend/client.py new file mode 100644 index 00000000..8abc47e2 --- /dev/null +++ b/src/crate/client/django/backend/client.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +from django.db.backends import BaseDatabaseClient +from crate.client.crash import main as crash_main + + +class DatabaseClient(BaseDatabaseClient): + executable_name = 'crash' + + def runshell(self): + """TODO: run shell""" + settings_dict = self.connection.settings_dict + + import sys + sys.argv = [sys.argv[0], "--hosts", settings_dict['SERVERS']] + crash_main() diff --git a/src/crate/client/django/backend/compiler.py b/src/crate/client/django/backend/compiler.py new file mode 100644 index 00000000..77f8aadd --- /dev/null +++ b/src/crate/client/django/backend/compiler.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +from django.db.models.sql import compiler + + +class SQLCompiler(compiler.SQLCompiler): + + def as_sql(self, with_limits=True, with_col_aliases=False): + sql, params = super(SQLCompiler, self).as_sql(with_limits=with_limits, with_col_aliases=with_col_aliases) + return sql.replace("%s", "?"), params + + +class SQLInsertCompiler(compiler.SQLInsertCompiler, SQLCompiler): + pass + + +class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler): + def as_sql(self): + """ + hack for converting djangos arguments placeholders into question marks + as crate-python uses dbapi option ``paramstyle=qmark`` which is not supported by + django + + TODO: make a Pull Request for django to support this dbapi option + :return: tuple of (, ) + """ + sql, params = super(SQLDeleteCompiler, self).as_sql() + return sql.replace("%s", "?"), params + + +class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler): + pass + + +class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler): + pass + + +class SQLDateCompiler(compiler.SQLDateCompiler, SQLCompiler): + pass + + +class SQLDateTimeCompiler(compiler.SQLDateTimeCompiler, SQLCompiler): + pass diff --git a/src/crate/client/django/backend/creation.py b/src/crate/client/django/backend/creation.py new file mode 100644 index 00000000..c1d08801 --- /dev/null +++ b/src/crate/client/django/backend/creation.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +from django.db.backends.creation import BaseDatabaseCreation + + +class DatabaseCreation(BaseDatabaseCreation): + data_types = { + 'AutoField': 'integer', + #'BinaryField': 'BLOB', + 'BooleanField': 'boolean', + 'CharField': 'string', + 'CommaSeparatedIntegerField': 'string', + #'DateField': 'timestamp', + 'DateTimeField': 'timestamp', + #'DecimalField': 'decimal', + 'FileField': 'string', + 'FilePathField': 'string', + 'FloatField': 'float', + 'IntegerField': 'integer', + 'BigIntegerField': 'long', + 'IPAddressField': 'ip', + 'GenericIPAddressField': 'string', + 'NullBooleanField': 'boolean', + 'OneToOneField': 'integer', + 'PositiveIntegerField': 'integer', + 'PositiveSmallIntegerField': 'short', + 'SlugField': 'string', + 'SmallIntegerField': 'short', + 'TextField': 'string', + #'TimeField': 'long', + } + + def sql_create_model(self, model, style, known_models=set()): + """ + issue a CREATE TABLE statement from a model + + TODO: support additional fields + + :return: list of sql statements, {} + """ + opts = model._meta + if not opts.managed or opts.proxy or opts.swapped: + return [], {} + final_output = [] + table_output = [] + qn = self.connection.ops.quote_name + for f in opts.local_fields: + col_type = f.db_type(connection=self.connection) + if col_type is None: + # Skip ManyToManyFields, because they're not represented as + # database columns in this table. + continue + + # Make the definition (e.g. 'foo VARCHAR(30)') for this field. + field_output = [style.SQL_FIELD(qn(f.column)), + style.SQL_COLTYPE(col_type)] + # Oracle treats the empty string ('') as null, so coerce the null + # option whenever '' is a possible value. + #null = f.null + #if (f.empty_strings_allowed and not f.primary_key and + # self.connection.features.interprets_empty_strings_as_nulls): + # null = True + #if not null: + # field_output.append(style.SQL_KEYWORD('NOT NULL')) + if f.primary_key: + field_output.append(style.SQL_KEYWORD('PRIMARY KEY')) + #elif f.unique: + # field_output.append(style.SQL_KEYWORD('UNIQUE')) + elif hasattr(f, "fulltext_index"): + # TODO: support compound index + fulltext_index = getattr(f, "fulltext_index", None) + analyzer = getattr(f, "analyzer", None) + if fulltext_index: + field_output.append(style.SQL_KEYWORD("INDEX USING FULLTEXT")) + if analyzer: + field_output.append(style.SQL_KEYWORD("WITH(analyzer='{}')".format(analyzer))) + + table_output.append(' '.join(field_output)) + + full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + + style.SQL_TABLE(qn(opts.db_table)) + ' ('] + for i, line in enumerate(table_output): # Combine and add commas. + full_statement.append( + ' %s%s' % (line, ',' if i < len(table_output) - 1 else '')) + full_statement.append(')') + + # CRATE TABLE PARAMS + crate_table_params = [] + crate_opts = getattr(model, "Crate", None) + if crate_opts: + number_of_replicas = getattr(crate_opts, "number_of_replicas", None) + clustered_by = getattr(crate_opts, "clustered_by", None) + number_of_shards = getattr(crate_opts, "number_of_shards", None) + if clustered_by is not None or number_of_shards is not None: + crate_table_params.append('CLUSTERED') + if clustered_by is not None: + crate_table_params.append('BY ({})'.format(qn(clustered_by))) + if number_of_shards is not None: + crate_table_params.append('INTO {} SHARDS'.format(number_of_shards)) + if number_of_replicas is not None: + crate_table_params.append('REPLICAS {}'.format(number_of_replicas)) + full_statement.append(' '.join(crate_table_params)) + + final_output.append('\n'.join(full_statement)) + + return final_output, {} + + def sql_for_inline_foreign_key_references(self, model, field, known_models, style): + """FOREIGN KEY not supported""" + return [], False + + def sql_for_pending_references(self, model, style, pending_references): + """ + should create ALTER TABLE statements + + not supported + """ + return [] + + def sql_indexes_for_model(self, model, style): + """ + should create CREATE INDEX statements + + not supported + """ + return [] + + def sql_indexes_for_field(self, model, f, style): + """not supported""" + return [] + + def sql_indexes_for_fields(self, model, fields, style): + """not supported""" + return [] + + def sql_destroy_model(self, model, references_to_delete, style): + """DROP TABLE""" + qn = self.connection.ops.quote_name + return ['%s %;' % (style.SQL_KEYWORD('DROP TABLE'), + style.SQL_TABLE(qn(model._meta.db_table)))] + + def sql_remove_table_constraints(self, model, references_to_delete, style): + """not supported""" + return [] + + def sql_destroy_indexes_for_model(self, model, style): + """not supported""" + return [] + + def sql_destroy_indexes_for_field(self, model, f, style): + """not supported""" + return [] + + def sql_destroy_indexes_for_fields(self, model, fields, style): + """not supported""" + return [] + + def _create_test_db(self, verbosity, autoclobber): + """cannot create dbs yet""" + return "" + + def _destroy_test_db(self, test_database_name, verbosity): + """cannot destroy dbs yet""" diff --git a/src/crate/client/django/backend/introspection.py b/src/crate/client/django/backend/introspection.py new file mode 100644 index 00000000..31dd1d0e --- /dev/null +++ b/src/crate/client/django/backend/introspection.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from django.db.backends import BaseDatabaseIntrospection + + +class DatabaseIntrospection(BaseDatabaseIntrospection): + data_types_reverse = { + "boolean": "BooleanField", + "byte": "SmallIntegerField", + "short": "SmallIntegerField", + "integer": "IntegerField", + "long": "BigIntegerField", + "float": "FloatField", + "double": "FloatField", # no double type in python + "timestamp": "DateTimeField", + "ip": "CharField", + "string": "CharField", + # TODO: object + } + + def get_table_list(self, cursor): + """TODO""" + tables = [] + cursor.execute( + "select table_name from information_schema.tables " + "where schema_name='doc'".format()) + for table_name in cursor.fetchall(): + if isinstance(table_name, list): + table_name = table_name[0] + tables.append(table_name) + return tables + + def sequence_list(self): + """sequences not supported""" + return [] + + def get_key_columns(self, cursor, table_name): + return [] + + def get_indexes(self, cursor, table_name): + indexes = {} + cursor.execute( + "select constraint_name from information_schema.table_constraints " + "where schema_name='doc' and table_name='{}'".format(table_name) + ) + for colname in cursor.fetchall(): + if isinstance(colname, list): + colname = colname[0] + indexes[colname] = { + 'primary_key': True, + 'unique': True + } + return indexes diff --git a/src/crate/client/django/backend/operations.py b/src/crate/client/django/backend/operations.py new file mode 100644 index 00000000..2c68fc19 --- /dev/null +++ b/src/crate/client/django/backend/operations.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +from django.db.backends import BaseDatabaseOperations + + +class DatabaseOperations(BaseDatabaseOperations): + + compiler_module = "crate.client.django.backend.compiler" + + def cache_key_culling_sql(self): + """not implemented""" + return None + + def distinct_sql(self, fields): + raise NotImplementedError("distinct on rows not implemented") + + def drop_foreignkey_sql(self): + """not supported""" + return '' + + def drop_sequence_sql(self, table): + """not supported""" + return '' + + def for_update_sql(self, nowait=False): + return '' + + def fulltext_search_sql(self, field_name): + return 'match({}, %s)'.format(field_name) + + def quote_name(self, name): + if name.startswith('"') and name.endswith('"'): + return name # Quoting once is enough. + return '"%s"' % name + + def sql_flush(self, style, tables, sequences, allow_cascade=False): + return [ + 'DELETE FROM {0}'.format(table) for table in tables + ] + + def start_transaction_sql(self): + return '' + + def end_transaction_sql(self, success=True): + return '' diff --git a/src/crate/client/django/backend/validation.py b/src/crate/client/django/backend/validation.py new file mode 100644 index 00000000..8d865d9d --- /dev/null +++ b/src/crate/client/django/backend/validation.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from django.db.backends import BaseDatabaseValidation + + +class DatabaseValidation(BaseDatabaseValidation): + pass diff --git a/src/crate/client/django/models/__init__.py b/src/crate/client/django/models/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/src/crate/client/django/models/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/src/crate/client/django/models/fields.py b/src/crate/client/django/models/fields.py new file mode 100644 index 00000000..c0458ddb --- /dev/null +++ b/src/crate/client/django/models/fields.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- + +from django.db.models import Field as _DjangoField +from django.db.models import ( + BooleanField as _DjangoBooleanField, + SmallIntegerField as _DjangoSmallIntegerField, + TextField as _DjangoTextField, + IntegerField as _DjangoIntegerField, + BigIntegerField as _DjangoBigIntegerField, + FloatField as _DjangoFloatField, + IPAddressField as _DjangoIPField +) + + +__all__ = [ + "BooleanField", + "StringField", + "ByteField", + "ShortField", + "IntegerField", + "LongField", + "FloatField", + "DoubleField", + "IPField", +] + + +class Field(_DjangoField): + + def __init__(self, *args, **kwargs): + self.fulltext_index = kwargs.pop("fulltext_index", None) + self.analyzer = kwargs.pop("analyzer", None) + super(Field, self).__init__(*args, **kwargs) + + def get_placeholder(self, idx, db): + return '?' + + +class BooleanField(_DjangoBooleanField, Field): + pass + + +class StringField(_DjangoTextField, Field): + pass + + +class ByteField(_DjangoIntegerField, Field): + pass + + +class ShortField(_DjangoSmallIntegerField, Field): + pass + + +class IntegerField(_DjangoIntegerField, Field): + pass + + +class LongField(_DjangoBigIntegerField, Field): + pass + + +class FloatField(_DjangoFloatField, Field): + pass + + +class DoubleField(_DjangoFloatField, Field): + pass + + +class IPField(_DjangoIPField, Field): + pass + +# TODO: ObjectField, TimeStampField diff --git a/src/crate/client/django/tests/__init__.py b/src/crate/client/django/tests/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/src/crate/client/django/tests/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/src/crate/client/django/tests/backend.txt b/src/crate/client/django/tests/backend.txt new file mode 100644 index 00000000..17ac9ef1 --- /dev/null +++ b/src/crate/client/django/tests/backend.txt @@ -0,0 +1,31 @@ + + >>> from crate.client.django.tests.models import User + >>> user = User.objects.create(id=1, username="mfelsche", slogan="Shut the fuck up!") + >>> user.id + 1 + + >>> user.username + 'mfelsche' + + >>> user.slogan + 'Shut the fuck up!' + + >>> del user + >>> user = User.objects.get(id=1) + >>> user.id + 1 + >>> user.username + 'mfelsche' + >>> user.slogan + 'Shut the fuck up!' + + >>> searched_user = User.objects.get(slogan__search='fuck') + >>> searched_user == user + True + + >>> user.delete() + + >>> User.objects.get(id=1) + Traceback (most recent call last): + ... + DoesNotExist: User matching query does not exist. diff --git a/src/crate/client/django/tests/models.py b/src/crate/client/django/tests/models.py new file mode 100644 index 00000000..f8c7074b --- /dev/null +++ b/src/crate/client/django/tests/models.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from crate.client.django.models.fields import * +from django.db.models import Model + + +class User(Model): + id = IntegerField(primary_key=True) + username = StringField() + slogan = StringField(fulltext_index=True, analyzer="english") + + class Crate: + number_of_replicas = 0 + number_of_shards = 4 + clustered_by = "id" diff --git a/src/crate/client/django/tests/settings.py b/src/crate/client/django/tests/settings.py new file mode 100644 index 00000000..69950a64 --- /dev/null +++ b/src/crate/client/django/tests/settings.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +DATABASES = { + 'default': { + 'ENGINE': 'crate.client.django.backend', + 'SERVERS': ['127.0.0.1:44209', ] + }, + 'other': { + 'ENGINE': 'crate.client.django.backend', + 'SERVERS': ['127.0.0.1:44209', ] + } +} + +SECRET_KEY = "0"*32 + +# Use a fast hasher to speed up tests. +PASSWORD_HASHERS = ( + 'django.contrib.auth.hashers.MD5PasswordHasher', +) + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + } +} + +INSTALLED_APPS = [ + "crate.client.django.tests" +] + +TEST_RUNNER = 'django.test.runner.DiscoverRunner' diff --git a/src/crate/client/tests.py b/src/crate/client/tests.py index 3ca1d283..01e26e88 100644 --- a/src/crate/client/tests.py +++ b/src/crate/client/tests.py @@ -26,6 +26,7 @@ import unittest import doctest import re + from pprint import pprint from datetime import datetime, date from six.moves import BaseHTTPServer @@ -75,6 +76,15 @@ def setUpMocked(test): crate_uri = "http://%s" % crate_host +def setUpForDjango(test): + connect(crate_host) + import os + os.environ["DJANGO_SETTINGS_MODULE"] = "crate.client.django.tests.settings" + + from django.test.runner import setup_databases + setup_databases(3, False) + + def setUpWithCrateLayer(test): test.globs['HttpClient'] = http.Client test.globs['crate_host'] = crate_host @@ -299,4 +309,14 @@ def test_suite(): s.layer = crate_layer suite.addTest(s) + # DJANGO TESTS + django_suite = doctest.DocFileSuite( + 'django/tests/backend.txt', + checker=checker, + setUp=setUpForDjango, + optionflags=flags + ) + django_suite.layer = crate_layer + suite.addTest(django_suite) + return suite diff --git a/versions.cfg b/versions.cfg index 27eacca2..0ca7c5b1 100644 --- a/versions.cfg +++ b/versions.cfg @@ -50,3 +50,5 @@ wheel = 0.22.0 # Required by: # crate==0.8.1 urllib3 = 1.7.1 + +django = 1.6.2