diff --git a/AUTHORS b/AUTHORS index e52fb46b47..e3db830b74 100644 --- a/AUTHORS +++ b/AUTHORS @@ -126,6 +126,7 @@ answer newbie questions, and generally made Django that much better: Oliver Rutherfurd Ivan Sagalaev (Maniac) David Schein + Pete Shinners sopel Thomas Steinacher Radek Švarz @@ -138,6 +139,7 @@ answer newbie questions, and generally made Django that much better: Amit Upadhyay Geert Vanderkelen Milton Waddams + Dan Watson Rachel Willmer wojtek ye7cakf02@sneakemail.com diff --git a/django/bin/compile-messages.py b/django/bin/compile-messages.py index 07dcce7bf6..0137ec8dd4 100755 --- a/django/bin/compile-messages.py +++ b/django/bin/compile-messages.py @@ -26,7 +26,10 @@ def compile_messages(): # See http://cyberelk.net/tim/articles/cmdline/ar01s02.html os.environ['djangocompilemo'] = pf + '.mo' os.environ['djangocompilepo'] = pf + '.po' - cmd = 'msgfmt -o "$djangocompilemo" "$djangocompilepo"' + if sys.platform == 'win32': # Different shell-variable syntax + cmd = 'msgfmt -o "%djangocompilemo%" "%djangocompilepo%"' + else: + cmd = 'msgfmt -o "$djangocompilemo" "$djangocompilepo"' os.system(cmd) if __name__ == "__main__": diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 3564be16a5..19996ad8f7 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -304,4 +304,18 @@ AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',) # TESTING # ########### -TEST_RUNNER='django.test.simple.run_tests' +# The name of the method to use to invoke the test suite +TEST_RUNNER = 'django.test.simple.run_tests' + +# The name of the database to use for testing purposes. +# If None, a name of 'test_' + DATABASE_NAME will be assumed +TEST_DATABASE_NAME = None + +# Tuple of other test databases to create. Names in this tuple +# are suffixes that will be appended to TEST_DATABASE_NAME +TEST_DATABASES = [] + +# Models to assign to each test database. This must be a list of +# dicts, with each dict key being a name from TEST_DATABASES and value +# a list of models or app_labels that will use that database. +TEST_DATABASE_MODELS = [] diff --git a/django/contrib/admin/views/doc.py b/django/contrib/admin/views/doc.py index b724cc5485..435d76f276 100644 --- a/django/contrib/admin/views/doc.py +++ b/django/contrib/admin/views/doc.py @@ -328,13 +328,17 @@ def extract_views_from_urlpatterns(urlpatterns, base=''): """ views = [] for p in urlpatterns: - if hasattr(p, 'get_callback'): + if hasattr(p, '_get_callback'): try: - views.append((p.get_callback(), base + p.regex.pattern)) + views.append((p._get_callback(), base + p.regex.pattern)) except ViewDoesNotExist: continue elif hasattr(p, '_get_url_patterns'): - views.extend(extract_views_from_urlpatterns(p.url_patterns, base + p.regex.pattern)) + try: + patterns = p.url_patterns + except ImportError: + continue + views.extend(extract_views_from_urlpatterns(patterns, base + p.regex.pattern)) else: raise TypeError, _("%s does not appear to be a urlpattern object") % p return views diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 4077237993..eb5713ba57 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -33,7 +33,7 @@ class Permission(models.Model): Permissions are set globally per type of object, not per specific object instance. It is possible to say "Mary may change news stories," but it's not currently possible to say "Mary may change news stories, but only the ones she created herself" or "Mary may only change news stories that have a certain status or publication date." - Three basic permissions -- add, create and delete -- are automatically created for each Django model. + Three basic permissions -- add, change and delete -- are automatically created for each Django model. """ name = models.CharField(_('name'), maxlength=50) content_type = models.ForeignKey(ContentType) diff --git a/django/contrib/sitemaps/__init__.py b/django/contrib/sitemaps/__init__.py new file mode 100644 index 0000000000..50f60b821e --- /dev/null +++ b/django/contrib/sitemaps/__init__.py @@ -0,0 +1,90 @@ +from django.core import urlresolvers +import urllib + +PING_URL = "http://www.google.com/webmasters/sitemaps/ping" + +class SitemapNotFound(Exception): + pass + +def ping_google(sitemap_url=None, ping_url=PING_URL): + """ + Alerts Google that the sitemap for the current site has been updated. + If sitemap_url is provided, it should be an absolute path to the sitemap + for this site -- e.g., '/sitemap.xml'. If sitemap_url is not provided, this + function will attempt to deduce it by using urlresolvers.reverse(). + """ + if sitemap_url is None: + try: + # First, try to get the "index" sitemap URL. + sitemap_url = urlresolvers.reverse('django.contrib.sitemap.views.index') + except urlresolvers.NoReverseMatch: + try: + # Next, try for the "global" sitemap URL. + sitemap_url = urlresolvers.reverse('django.contrib.sitemap.views.sitemap') + except urlresolvers.NoReverseMatch: + pass + + if sitemap_url is None: + raise SitemapNotFound("You didn't provide a sitemap_url, and the sitemap URL couldn't be auto-detected.") + + from django.contrib.sites.models import Site + current_site = Site.objects.get_current() + url = "%s%s" % (current_site.domain, sitemap) + params = urllib.urlencode({'sitemap':url}) + urllib.urlopen("%s?%s" % (ping_url, params)) + +class Sitemap: + def __get(self, name, obj, default=None): + try: + attr = getattr(self, name) + except AttributeError: + return default + if callable(attr): + return attr(obj) + return attr + + def items(self): + return [] + + def location(self, obj): + return obj.get_absolute_url() + + def get_urls(self): + from django.contrib.sites.models import Site + current_site = Site.objects.get_current() + urls = [] + for item in self.items(): + loc = "http://%s%s" % (current_site.domain, self.__get('location', item)) + url_info = { + 'location': loc, + 'lastmod': self.__get('lastmod', item, None), + 'changefreq': self.__get('changefreq', item, None), + 'priority': self.__get('priority', item, None) + } + urls.append(url_info) + return urls + +class FlatPageSitemap(Sitemap): + def items(self): + from django.contrib.sites.models import Site + current_site = Site.objects.get_current() + return current_site.flatpage_set.all() + +class GenericSitemap(Sitemap): + priority = None + changefreq = None + + def __init__(self, info_dict, priority=None, changefreq=None): + self.queryset = info_dict['queryset'] + self.date_field = info_dict.get('date_field', None) + self.priority = priority + self.changefreq = changefreq + + def items(self): + # Make sure to return a clone; we don't want premature evaluation. + return self.queryset.filter() + + def lastmod(self, item): + if self.date_field is not None: + return getattr(item, self.date_field) + return None diff --git a/django/contrib/sitemaps/templates/sitemap.xml b/django/contrib/sitemaps/templates/sitemap.xml new file mode 100644 index 0000000000..3ee4f914f7 --- /dev/null +++ b/django/contrib/sitemaps/templates/sitemap.xml @@ -0,0 +1,11 @@ + + +{% for url in urlset %} + + {{ url.location|escape }} + {% if url.lastmod %}{{ url.lastmod|date:"Y-m-d" }}{% endif %} + {% if url.changefreq %}{{ url.changefreq }}{% endif %} + {% if url.priority %}{{ url.priority }}{% endif %} + +{% endfor %} + diff --git a/django/contrib/sitemaps/templates/sitemap_index.xml b/django/contrib/sitemaps/templates/sitemap_index.xml new file mode 100644 index 0000000000..e9d722ac7f --- /dev/null +++ b/django/contrib/sitemaps/templates/sitemap_index.xml @@ -0,0 +1,8 @@ + + +{% for location in sitemaps %} + + {{ location|escape }} + +{% endfor %} + diff --git a/django/contrib/sitemaps/views.py b/django/contrib/sitemaps/views.py new file mode 100644 index 0000000000..8a4592c3e4 --- /dev/null +++ b/django/contrib/sitemaps/views.py @@ -0,0 +1,30 @@ +from django.http import HttpResponse, Http404 +from django.template import loader +from django.contrib.sites.models import Site +from django.core import urlresolvers + +def index(request, sitemaps): + current_site = Site.objects.get_current() + sites = [] + protocol = request.is_secure() and 'https' or 'http' + for section in sitemaps.keys(): + sitemap_url = urlresolvers.reverse('django.contrib.sitemap.views.sitemap', kwargs={'section': section}) + sites.append('%s://%s%s' % (protocol, current_site.domain, sitemap_url)) + xml = loader.render_to_string('sitemap_index.xml', {'sitemaps': sites}) + return HttpResponse(xml, mimetype='application/xml') + +def sitemap(request, sitemaps, section=None): + maps, urls = [], [] + if section is not None: + if not sitemaps.has_key(section): + raise Http404("No sitemap available for section: %r" % section) + maps.append(sitemaps[section]) + else: + maps = sitemaps.values() + for site in maps: + if callable(site): + urls.extend(site().get_urls()) + else: + urls.extend(site.get_urls()) + xml = loader.render_to_string('sitemap.xml', {'urlset': urls}) + return HttpResponse(xml, mimetype='application/xml') diff --git a/django/core/management.py b/django/core/management.py index 028b4d89a5..c1454c00d6 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -294,8 +294,7 @@ def syncdb(verbosity=2, interactive=True): except ImportError: pass - # Send the post_syncdb signal, so individual apps can do whatever they need - # to do at this point. + # Install each app for app in models.get_apps(): # Install each application (models already installed will be skipped) created = _install(app, commit=False, initial_data=False) @@ -303,10 +302,14 @@ def syncdb(verbosity=2, interactive=True): for model in created: print "Created table %s" % model._meta.db_table created_models.extend(created) + transaction.commit_unless_managed() + + # Send the post_syncdb signal, so individual apps can do whatever they need + # to do at this point. + for app in models.get_apps(): dispatcher.send(signal=signals.post_syncdb, sender=app, app=app, created_models=created_models, verbosity=verbosity, interactive=interactive) - transaction.commit_unless_managed() # Install initial data for the app (but only if this is a model we've # just created) diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index e939c0c6e7..fb293c7c13 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -11,7 +11,7 @@ from django.db import models class SerializationError(Exception): """Something bad happened during serialization.""" pass - + class DeserializationError(Exception): """Something bad happened during deserialization.""" pass @@ -20,15 +20,15 @@ class Serializer(object): """ Abstract serializer base class. """ - + def serialize(self, queryset, **options): """ Serialize a queryset. """ self.options = options - + self.stream = options.get("stream", StringIO()) - + self.start_serialization() for obj in queryset: self.start_object(obj) @@ -44,61 +44,65 @@ class Serializer(object): self.end_object(obj) self.end_serialization() return self.getvalue() - + def get_string_value(self, obj, field): """ Convert a field's value to a string. """ if isinstance(field, models.DateTimeField): - value = getattr(obj, field.name).strftime("%Y-%m-%d %H:%M:%S") + value = getattr(obj, field.name) + if value is None: + value = '' + else: + value = value.strftime("%Y-%m-%d %H:%M:%S") elif isinstance(field, models.FileField): value = getattr(obj, "get_%s_url" % field.name, lambda: None)() else: value = field.flatten_data(follow=None, obj=obj).get(field.name, "") return str(value) - + def start_serialization(self): """ Called when serializing of the queryset starts. """ raise NotImplementedError - + def end_serialization(self): """ Called when serializing of the queryset ends. """ pass - + def start_object(self, obj): """ Called when serializing of an object starts. """ raise NotImplementedError - + def end_object(self, obj): """ Called when serializing of an object ends. """ pass - + def handle_field(self, obj, field): """ Called to handle each individual (non-relational) field on an object. """ raise NotImplementedError - + def handle_fk_field(self, obj, field): """ Called to handle a ForeignKey field. """ raise NotImplementedError - + def handle_m2m_field(self, obj, field): """ Called to handle a ManyToManyField. """ raise NotImplementedError - + def getvalue(self): """ Return the fully serialized queryset. @@ -109,7 +113,7 @@ class Deserializer(object): """ Abstract base deserializer class. """ - + def __init__(self, stream_or_string, **options): """ Init this serializer given a stream or a string @@ -123,39 +127,39 @@ class Deserializer(object): # deserialization starts (otherwise subclass calls to get_model() # and friends might fail...) models.get_apps() - + def __iter__(self): return self - + def next(self): """Iteration iterface -- return the next item in the stream""" raise NotImplementedError - + class DeserializedObject(object): """ A deserialzed model. - + Basically a container for holding the pre-saved deserialized data along with the many-to-many data saved with the object. - + Call ``save()`` to save the object (with the many-to-many data) to the database; call ``save(save_m2m=False)`` to save just the object fields (and not touch the many-to-many stuff.) """ - + def __init__(self, obj, m2m_data=None): self.object = obj self.m2m_data = m2m_data - + def __repr__(self): return "" % str(self.object) - + def save(self, save_m2m=True): self.object.save() if self.m2m_data and save_m2m: for accessor_name, object_list in self.m2m_data.items(): setattr(self.object, accessor_name, object_list) - - # prevent a second (possibly accidental) call to save() from saving + + # prevent a second (possibly accidental) call to save() from saving # the m2m data twice. self.m2m_data = None diff --git a/django/core/serializers/json.py b/django/core/serializers/json.py index a8b4259099..72234a624b 100644 --- a/django/core/serializers/json.py +++ b/django/core/serializers/json.py @@ -48,4 +48,4 @@ class DateTimeAwareJSONEncoder(simplejson.JSONEncoder): elif isinstance(o, datetime.time): return o.strftime(self.TIME_FORMAT) else: - return super(self, DateTimeAwareJSONEncoder).default(o) \ No newline at end of file + return super(DateTimeAwareJSONEncoder, self).default(o) diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index 308bc12aa8..10108f08b8 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -11,10 +11,6 @@ except ImportError, e: from django.core.exceptions import ImproperlyConfigured raise ImproperlyConfigured, "Error loading psycopg2 module: %s" % e -# Register Unicode conversions -import psycopg2.extensions -psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) - DatabaseError = Database.DatabaseError try: @@ -47,6 +43,7 @@ class DatabaseWrapper(local): self.connection = Database.connect(conn_string) self.connection.set_isolation_level(1) # make transactions transparent to all cursors cursor = self.connection.cursor() + cursor.tzinfo_factory = None cursor.execute("SET TIME ZONE %s", [settings.TIME_ZONE]) if settings.DEBUG: return util.CursorDebugWrapper(cursor, self) diff --git a/django/db/backends/util.py b/django/db/backends/util.py index 74d33f42ca..88318941c8 100644 --- a/django/db/backends/util.py +++ b/django/db/backends/util.py @@ -98,7 +98,7 @@ def rev_typecast_boolean(obj, d): def _dict_helper(desc, row): "Returns a dictionary for the given cursor.description and result row." - return dict([(desc[col[0]][0], col[1]) for col in enumerate(row)]) + return dict(zip([col[0] for col in desc], row)) def dictfetchone(cursor): "Returns a row from the cursor as a dict" diff --git a/django/template/__init__.py b/django/template/__init__.py index 2f68924d18..4cf3304eb6 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -60,8 +60,6 @@ from django.conf import settings from django.template.context import Context, RequestContext, ContextPopException from django.utils.functional import curry from django.utils.text import smart_split -from django.dispatch import dispatcher -from django.template import signals __all__ = ('Template', 'Context', 'RequestContext', 'compile_string') @@ -139,14 +137,13 @@ class StringOrigin(Origin): return self.source class Template(object): - def __init__(self, template_string, origin=None, name=''): + def __init__(self, template_string, origin=None): "Compilation stage" if settings.TEMPLATE_DEBUG and origin == None: origin = StringOrigin(template_string) # Could do some crazy stack-frame stuff to record where this string # came from... self.nodelist = compile_string(template_string, origin) - self.name = name def __iter__(self): for node in self.nodelist: @@ -155,7 +152,6 @@ class Template(object): def render(self, context): "Display stage -- can be called many times" - dispatcher.send(signal=signals.template_rendered, sender=self, template=self, context=context) return self.nodelist.render(context) def compile_string(template_string, origin): @@ -618,11 +614,7 @@ def resolve_variable(path, context): (The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.') """ - if path == 'False': - current = False - elif path == 'True': - current = True - elif path[0].isdigit(): + if path[0].isdigit(): number_type = '.' in path and float or int try: current = number_type(path) diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index d111ddca89..0a4fe33d82 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -251,7 +251,7 @@ class SsiNode(Node): output = '' if self.parsed: try: - t = Template(output, name=self.filepath) + t = Template(output) return t.render(context) except TemplateSyntaxError, e: if settings.DEBUG: diff --git a/django/template/loader.py b/django/template/loader.py index 03e6f8d49d..60f24554f1 100644 --- a/django/template/loader.py +++ b/django/template/loader.py @@ -76,16 +76,14 @@ def get_template(template_name): Returns a compiled Template object for the given template name, handling template inheritance recursively. """ - source, origin = find_template_source(template_name) - template = get_template_from_string(source, origin, template_name) - return template + return get_template_from_string(*find_template_source(template_name)) -def get_template_from_string(source, origin=None, name=None): +def get_template_from_string(source, origin=None): """ Returns a compiled Template object for the given template code, handling template inheritance recursively. """ - return Template(source, origin, name) + return Template(source, origin) def render_to_string(template_name, dictionary=None, context_instance=None): """ diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py index b20c318f1f..7f22f207b6 100644 --- a/django/template/loader_tags.py +++ b/django/template/loader_tags.py @@ -57,7 +57,7 @@ class ExtendsNode(Node): except TemplateDoesNotExist: raise TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent else: - return get_template_from_string(source, origin, parent) + return get_template_from_string(source, origin) def render(self, context): compiled_parent = self.get_parent(context) diff --git a/django/test/simple.py b/django/test/simple.py index e72f693459..2469f80b3a 100644 --- a/django/test/simple.py +++ b/django/test/simple.py @@ -61,7 +61,8 @@ def run_tests(module_list, verbosity=1, extra_tests=[]): for test in extra_tests: suite.addTest(test) - old_name = create_test_db(verbosity) + old_name = settings.DATABASE_NAME + create_test_db(verbosity) management.syncdb(verbosity, interactive=False) unittest.TextTestRunner(verbosity=verbosity).run(suite) destroy_test_db(old_name, verbosity) diff --git a/django/test/utils.py b/django/test/utils.py index 28db46dafd..bde323fa4e 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -1,6 +1,6 @@ import sys, time from django.conf import settings -from django.db import connection, transaction +from django.db import backend, connect, connection, connection_info, connections # The prefix to put on the default database name when creating # the test database. @@ -12,67 +12,90 @@ def _set_autocommit(connection): connection.connection.autocommit(True) elif hasattr(connection.connection, "set_isolation_level"): connection.connection.set_isolation_level(0) - + def create_test_db(verbosity=1, autoclobber=False): if verbosity >= 1: print "Creating test database..." + # If we're using SQLite, it's more convenient to test against an # in-memory database. if settings.DATABASE_ENGINE == "sqlite3": TEST_DATABASE_NAME = ":memory:" + if verbosity >= 2: + print "Using in-memory sqlite database for testing" else: - TEST_DATABASE_NAME = TEST_DATABASE_PREFIX + settings.DATABASE_NAME - + if settings.TEST_DATABASE_NAME: + TEST_DATABASE_NAME = settings.TEST_DATABASE_NAME + else: + TEST_DATABASE_NAME = TEST_DATABASE_PREFIX + settings.DATABASE_NAME + + qn = backend.quote_name # Create the test database and connect to it. We need to autocommit # if the database supports it because PostgreSQL doesn't allow # CREATE/DROP DATABASE statements within transactions. cursor = connection.cursor() _set_autocommit(connection) try: - cursor.execute("CREATE DATABASE %s" % TEST_DATABASE_NAME) + cursor.execute("CREATE DATABASE %s" % qn(db_name)) except Exception, e: sys.stderr.write("Got an error creating the test database: %s\n" % e) if not autoclobber: - confirm = raw_input("It appears the test database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % TEST_DATABASE_NAME) + confirm = raw_input("It appears the test database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % db_name) if autoclobber or confirm == 'yes': try: if verbosity >= 1: print "Destroying old test database..." - cursor.execute("DROP DATABASE %s" % TEST_DATABASE_NAME) + cursor.execute("DROP DATABASE %s" % qn(db_name)) if verbosity >= 1: print "Creating test database..." - cursor.execute("CREATE DATABASE %s" % TEST_DATABASE_NAME) + cursor.execute("CREATE DATABASE %s" % qn(db_name)) except Exception, e: sys.stderr.write("Got an error recreating the test database: %s\n" % e) sys.exit(2) else: print "Tests cancelled." sys.exit(1) - - connection.close() - old_database_name = settings.DATABASE_NAME - settings.DATABASE_NAME = TEST_DATABASE_NAME + # Close the old connection + connection.close() - # Get a cursor (even though we don't need one yet). This has - # the side effect of initializing the test database. - cursor = connection.cursor() - - return old_database_name + # Get a cursor (even though we don't need one yet). This has + # the side effect of initializing the test database. + cursor = connection.cursor() -def destroy_test_db(old_database_name, verbosity=1): + # Fill OTHER_DATABASES with the TEST_DATABASES settings, + # and connect each named connection to the test database, using + # a separate connection instance for each (so, eg, transactions don't + # collide) + test_databases = {} + for db_name in settings.TEST_DATABASES: + if settings.DATABASE_ENGINE == 'sqlite3': + full_name = TEST_DATABASE_NAME + else: + full_name = TEST_DATABASE_NAME + db_name + db_st = {'DATABASE_NAME': full_name} + if db_name in settings.TEST_DATABASE_MODELS: + db_st['MODELS'] = settings.TEST_DATABASE_MODELS.get(db_name, []) + test_databases[db_name] = db_st + connections[db_name] = connect(connection_info.settings) + connections[db_name].connection.cursor() # Initialize it + settings.OTHER_DATABASES = test_databases + +def destroy_test_db(old_database_name, old_databases, verbosity=1): # Unless we're using SQLite, remove the test database to clean up after # ourselves. Connect to the previous database (not the test database) # to do so, because it's not allowed to delete a database while being # connected to it. if verbosity >= 1: print "Destroying test database..." - if settings.DATABASE_ENGINE != "sqlite3": + if settings.DATABASE_ENGINE != "sqlite3": connection.close() TEST_DATABASE_NAME = settings.DATABASE_NAME settings.DATABASE_NAME = old_database_name + settings.OTHER_DATABASES = old_databases cursor = connection.cursor() _set_autocommit(connection) time.sleep(1) # To avoid "database is being accessed by other users" errors. - cursor.execute("DROP DATABASE %s" % TEST_DATABASE_NAME) + cursor.execute("DROP DATABASE %s" % backend.quote_name(TEST_DATABASE_NAME)) connection.close() + diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py index 632e804f26..6aef313d35 100644 --- a/django/utils/datastructures.py +++ b/django/utils/datastructures.py @@ -187,17 +187,23 @@ class MultiValueDict(dict): "Returns a copy of this object." return self.__deepcopy__() - def update(self, other_dict): - "update() extends rather than replaces existing key lists." - if isinstance(other_dict, MultiValueDict): - for key, value_list in other_dict.lists(): - self.setlistdefault(key, []).extend(value_list) - else: - try: - for key, value in other_dict.items(): - self.setlistdefault(key, []).append(value) - except TypeError: - raise ValueError, "MultiValueDict.update() takes either a MultiValueDict or dictionary" + def update(self, *args, **kwargs): + "update() extends rather than replaces existing key lists. Also accepts keyword args." + if len(args) > 1: + raise TypeError, "update expected at most 1 arguments, got %d", len(args) + if args: + other_dict = args[0] + if isinstance(other_dict, MultiValueDict): + for key, value_list in other_dict.lists(): + self.setlistdefault(key, []).extend(value_list) + else: + try: + for key, value in other_dict.items(): + self.setlistdefault(key, []).append(value) + except TypeError: + raise ValueError, "MultiValueDict.update() takes either a MultiValueDict or dictionary" + for key, value in kwargs.iteritems(): + self.setlistdefault(key, []).append(value) class DotExpandedDict(dict): """ diff --git a/django/views/debug.py b/django/views/debug.py index f15eb8ff01..6934360afd 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -115,7 +115,7 @@ def technical_500_response(request, exc_type, exc_value, tb): 'function': '?', 'lineno': '?', }] - t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 Template') + t = Template(TECHNICAL_500_TEMPLATE) c = Context({ 'exception_type': exc_type.__name__, 'exception_value': exc_value, @@ -141,7 +141,7 @@ def technical_404_response(request, exception): # tried exists but is an empty list. The URLconf must've been empty. return empty_urlconf(request) - t = Template(TECHNICAL_404_TEMPLATE, name='Technical 404 Template') + t = Template(TECHNICAL_404_TEMPLATE) c = Context({ 'root_urlconf': settings.ROOT_URLCONF, 'urlpatterns': tried, @@ -154,7 +154,7 @@ def technical_404_response(request, exception): def empty_urlconf(request): "Create an empty URLconf 404 error response." - t = Template(EMPTY_URLCONF_TEMPLATE, name='Empty URLConf Template') + t = Template(EMPTY_URLCONF_TEMPLATE) c = Context({ 'project_name': settings.SETTINGS_MODULE.split('.')[0] }) diff --git a/django/views/static.py b/django/views/static.py index a8c8328014..ac323944d0 100644 --- a/django/views/static.py +++ b/django/views/static.py @@ -81,7 +81,7 @@ def directory_index(path, fullpath): try: t = loader.get_template('static/directory_index') except TemplateDoesNotExist: - t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE, name='Default Directory Index Template') + t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE) files = [] for f in os.listdir(fullpath): if not f.startswith('.'): diff --git a/docs/add_ons.txt b/docs/add_ons.txt index 6507f3b139..a0377700d7 100644 --- a/docs/add_ons.txt +++ b/docs/add_ons.txt @@ -153,6 +153,15 @@ See the `sites documentation`_. .. _sites documentation: http://www.djangoproject.com/documentation/sites/ +sitemaps +======== + +A framework for generating Google sitemap XML files. + +See the `sitemaps documentation`_. + +.. _sitemaps documentation: http://www.djangoproject.com/documentation/sitemaps/ + syndication =========== diff --git a/docs/contributing.txt b/docs/contributing.txt index 9d116cac10..3d101c3241 100644 --- a/docs/contributing.txt +++ b/docs/contributing.txt @@ -168,6 +168,19 @@ Please follow these coding standards when writing code for inclusion in Django: {{foo}} + * In Django views, the first parameter in a view function should be called + ``request``. + + Do this:: + + def my_view(request, foo): + # ... + + Don't do this:: + + def my_view(req, foo): + # ... + * Please don't put your name in the code. While we appreciate all contributions to Django, our policy is not to publish individual developer names in code -- for instance, at the top of Python modules. diff --git a/docs/django-admin.txt b/docs/django-admin.txt index eb7b2dccd6..672200c5e7 100644 --- a/docs/django-admin.txt +++ b/docs/django-admin.txt @@ -292,6 +292,13 @@ this command to install the default apps. If you're installing the ``django.contrib.auth`` application, ``syncdb`` will give you the option of creating a superuser immediately. +test +---- + +Discover and run tests for all installed models. See `Testing Django applications`_ for more information. + +.. _testing django applications: ../testing/ + validate -------- @@ -338,6 +345,17 @@ setting the Python path for you. Displays a help message that includes a terse list of all available actions and options. +--noinput +--------- + +Inform django-admin that the user should NOT be prompted for any input. Useful if +the django-admin script will be executed as an unattended, automated script. + +--noreload +---------- + +Disable the use of the auto-reloader when running the development server. + --version --------- @@ -348,6 +366,17 @@ Example output:: 0.9.1 0.9.1 (SVN) +--verbosity +----------- + +Example usage:: + + django-admin.py syncdb --verbosity=2 + +Verbosity determines the amount of notification and debug information that +will be printed to the console. '0' is no output, '1' is normal output, +and `2` is verbose output. + Extra niceties ============== diff --git a/docs/faq.txt b/docs/faq.txt index d39e203c76..204c69244d 100644 --- a/docs/faq.txt +++ b/docs/faq.txt @@ -103,10 +103,10 @@ Lawrence, Kansas, USA. On IRC, Simon goes by ``SimonW``. `Wilson Miner`_ - Wilson's design-fu makes us all look like rock stars. By day, he's an + Wilson's design-fu makes us all look like rock stars. By day, he's an interactive designer for `Apple`. Don't ask him what he's working on, or he'll have to kill you. He lives in San Francisco. - + On IRC, Wilson goes by ``wilsonian``. .. _`World Online`: http://code.djangoproject.com/wiki/WorldOnline @@ -641,7 +641,7 @@ How can I get started contributing code to Django? Thanks for asking! We've written an entire document devoted to this question. It's titled `Contributing to Django`_. -.. _Contributing do Django: http://www.djangoproject.com/documentation/contributing/ +.. _Contributing to Django: http://www.djangoproject.com/documentation/contributing/ I submitted a bug fix in the ticket system several weeks ago. Why are you ignoring my patch? -------------------------------------------------------------------------------------------- diff --git a/docs/settings.txt b/docs/settings.txt index d9df111155..b927b62ca7 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -754,6 +754,30 @@ misspelled) variables. See `How invalid variables are handled`_. .. _How invalid variables are handled: http://www.djangoproject.com/documentation/templates_python/#how-invalid-variables-are-handled +TEST_RUNNER +----------- + +**New in Django development version** + +Default: ``'django.test.simple.run_tests'`` + +The name of the method to use for starting the test suite. See +`Testing Django Applications`_. + +.. _Testing Django Applications: ../testing/ + +TEST_DATABASE_NAME +------------------ + +**New in Django development version** + +Default: ``None`` + +The name of database to use when running the test suite. If a value of +``None`` is specified, the test database will use the name ``'test_' + settings.DATABASE_NAME``. See `Testing Django Applications`_. + +.. _Testing Django Applications: ../testing/ + TIME_FORMAT ----------- diff --git a/docs/sitemaps.txt b/docs/sitemaps.txt new file mode 100644 index 0000000000..fec65572f2 --- /dev/null +++ b/docs/sitemaps.txt @@ -0,0 +1,320 @@ +===================== +The sitemap framework +===================== + +**New in Django development version**. + +Django comes with a high-level sitemap-generating framework that makes +creating `Google Sitemap`_ XML files easy. + +.. _Google Sitemap: http://www.google.com/webmasters/sitemaps/docs/en/protocol.html + +Overview +======== + +A sitemap is an XML file on your Web site that tells search-engine indexers how +frequently your pages change and how "important" certain pages are in relation +to other pages on your site. This information helps search engines index your +site. + +The Django sitemap framework automates the creation of this XML file by letting +you express this information in Python code. + +It works much like Django's `syndication framework`_. To create a sitemap, just +write a ``Sitemap`` class and point to it in your URLconf_. + +.. _syndication framework: http://www.djangoproject.com/documentation/syndication/ +.. _URLconf: http://www.djangoproject.com/documentation/url_dispatch/ + +Installation +============ + +To install the sitemap app, follow these steps: + + 1. Add ``'django.contrib.sitemaps'`` to your INSTALLED_APPS_ setting. + 2. Make sure ``'django.template.loaders.app_directories.load_template_source'`` + is in your TEMPLATE_LOADERS_ setting. It's in there by default, so + you'll only need to change this if you've changed that setting. + 3. Make sure you've installed the `sites framework`_. + +(Note: The sitemap application doesn't install any database tables. The only +reason it needs to go into ``INSTALLED_APPS`` is so that the +``load_template_source`` template loader can find the default templates.) + +.. _INSTALLED_APPS: http://www.djangoproject.com/documentation/settings/#installed-apps +.. _TEMPLATE_LOADERS: http://www.djangoproject.com/documentation/settings/#template-loaders +.. _sites framework: http://www.djangoproject.com/documentation/sites/ + +Initialization +============== + +To activate sitemap generation on your Django site, add this line to your +URLconf_: + + (r'^sitemap.xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps}) + +This tells Django to build a sitemap when a client accesses ``/sitemap.xml``. + +The name of the sitemap file is not important, but the location is. Google will +only index links in your sitemap for the current URL level and below. For +instance, if ``sitemap.xml`` lives in your root directory, it may reference any +URL in your site. However, if your sitemap lives at ``/content/sitemap.xml``, +it may only reference URLs that begin with ``/content/``. + +The sitemap view takes an extra, required argument: ``{'sitemaps': sitemaps}``. +``sitemaps`` should be a dictionary that maps a short section label (e.g., +``blog`` or ``news``) to its ``Sitemap`` class (e.g., ``BlogSitemap`` or +``NewsSitemap``). It may also map to an *instance* of a ``Sitemap`` class +(e.g., ``BlogSitemap(some_var)``). + +.. _URLconf: http://www.djangoproject.com/documentation/url_dispatch/ + +Sitemap classes +=============== + +A ``Sitemap`` class is a simple Python class that represents a "section" of +entries in your sitemap. For example, one ``Sitemap`` class could represent all +the entries of your weblog, while another could represent all of the events in +your events calendar. + +In the simplest case, all these sections get lumped together into one +``sitemap.xml``, but it's also possible to use the framework to generate a +sitemap index that references individual sitemap files, one per section. (See +`Creating a sitemap index`_ below.) + +``Sitemap`` classes must subclass ``django.contrib.sitemaps.Sitemap``. They can +live anywhere in your codebase. + +A simple example +================ + +Let's assume you have a blog system, with an ``Entry`` model, and you want your +sitemap to include all the links to your individual blog entries. Here's how +your sitemap class might look:: + + from django.contrib.sitemaps import Sitemap + from mysite.blog.models import Entry + + class BlogSitemap(Sitemap): + changefreq = "never" + priority = 0.5 + + def items(self): + return Entry.objects.filter(is_draft=False) + + def lastmod(self, obj): + return obj.pub_date + +Note: + + * ``changefreq`` and ``priority`` are class attributes corresponding to + ```` and ```` elements, respectively. They can be + made callable as functions, as ``lastmod`` was in the example. + * ``items()`` is simply a method that returns a list of objects. The objects + returned will get passed to any callable methods corresponding to a + sitemap property (``location``, ``lastmod``, ``changefreq``, and + ``priority``). + * ``lastmod`` should return a Python ``datetime`` object. + * There is no ``location`` method in this example, but you can provide it + in order to specify the URL for your object. By default, ``location()`` + calls ``get_absolute_url()`` on each object and returns the result. + +Sitemap class reference +======================= + +A ``Sitemap`` class can define the following methods/attributes: + +``items`` +--------- + +**Required.** A method that returns a list of objects. The framework doesn't +care what *type* of objects they are; all that matters is that these objects +get passed to the ``location()``, ``lastmod()``, ``changefreq()`` and +``priority()`` methods. + +``location`` +------------ + +**Optional.** Either a method or attribute. + +If it's a method, it should return the absolute URL for a given object as +returned by ``items()``. + +If it's an attribute, its value should be a string representing an absolute URL +to use for *every* object returned by ``items()``. + +In both cases, "absolute URL" means a URL that doesn't include the protocol or +domain. Examples: + + * Good: ``'/foo/bar/'`` + * Bad: ``'example.com/foo/bar/'`` + * Bad: ``'http://example.com/foo/bar/'`` + +If ``location`` isn't provided, the framework will call the +``get_absolute_url()`` method on each object as returned by ``items()``. + +``lastmod`` +----------- + +**Optional.** Either a method or attribute. + +If it's a method, it should take one argument -- an object as returned by +``items()`` -- and return that object's last-modified date/time, as a Python +``datetime.datetime`` object. + +If it's an attribute, its value should be a Python ``datetime.datetime`` object +representing the last-modified date/time for *every* object returned by +``items()``. + +``changefreq`` +-------------- + +**Optional.** Either a method or attribute. + +If it's a method, it should take one argument -- an object as returned by +``items()`` -- and return that object's change frequency, as a Python string. + +If it's an attribute, its value should be a string representing the change +frequency of *every* object returned by ``items()``. + +Possible values for ``changefreq``, whether you use a method or attribute, are: + + * ``'always'`` + * ``'hourly'`` + * ``'daily'`` + * ``'weekly'`` + * ``'monthly'`` + * ``'yearly'`` + * ``'never'`` + +``priority`` +------------ + +**Optional.** Either a method or attribute. + +If it's a method, it should take one argument -- an object as returned by +``items()`` -- and return that object's priority, as either a string or float. + +If it's an attribute, its value should be either a string or float representing +the priority of *every* object returned by ``items()``. + +Example values for ``priority``: ``0.4``, ``1.0``. The default priority of a +page is ``0.5``. See Google's documentation for more documentation. + +.. _Google's documentation: http://www.google.com/webmasters/sitemaps/docs/en/protocol.html + +Shortcuts +========= + +The sitemap framework provides a couple convenience classes for common cases: + +``FlatPageSitemap`` +------------------- + +The ``django.contrib.sitemaps.FlatPageSitemap`` class looks at all flatpages_ +defined for the current ``SITE_ID`` (see the `sites documentation`_) and +creates an entry in the sitemap. These entries include only the ``location`` +attribute -- not ``lastmod``, ``changefreq`` or ``priority``. + +.. _flatpages: http://www.djangoproject.com/documentation/flatpages/ +.. _sites documentation: http://www.djangoproject.com/documentation/sites/ + +``GenericSitemap`` +------------------ + +The ``GenericSitemap`` class works with any `generic views`_ you already have. +To use it, create an instance, passing in the same ``info_dict`` you pass to +the generic views. The only requirement is that the dictionary have a +``queryset`` entry. It may also have a ``date_field`` entry that specifies a +date field for objects retrieved from the ``queryset``. This will be used for +the ``lastmod`` attribute in the generated sitemap. You may also pass +``priority`` and ``changefreq`` keyword arguments to the ``GenericSitemap`` +constructor to specify these attributes for all URLs. + +.. _generic views: http://www.djangoproject.com/documentation/generic_views/ + +Example +------- + +Here's an example of a URLconf_ using both:: + + from django.conf.urls.defaults import * + from django.contrib.sitemaps import FlatPageSitemap, GenericSitemap + from mysite.blog.models import Entry + + info_dict = { + 'queryset': Entry.objects.all(), + 'date_field': 'pub_date', + } + + sitemaps = { + 'flatpages': FlatPageSitemap, + 'blog': GenericSitemap(info_dict, priority=0.6), + } + + urlpatterns = patterns('', + # some generic view using info_dict + # ... + + # the sitemap + (r'^sitemap.xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps}) + ) + +.. _URLconf: http://www.djangoproject.com/documentation/url_dispatch/ + +Creating a sitemap index +======================== + +The sitemap framework also has the ability to create a sitemap index that +references individual sitemap files, one per each section defined in your +``sitemaps`` dictionary. The only differences in usage are: + + * You use two views in your URLconf: ``django.contrib.sitemaps.views.index`` + and ``django.contrib.sitemaps.views.sitemap``. + * The ``django.contrib.sitemaps.views.sitemap`` view should take a + ``section`` keyword argument. + +Here is what the relevant URLconf lines would look like for the example above:: + + (r'^sitemap.xml$', 'django.contrib.sitemaps.views.index', {'sitemaps': sitemaps}) + (r'^sitemap-(?P
.+).xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps}) + +This will automatically generate a ``sitemap.xml`` file that references +both ``sitemap-flatpages.xml`` and ``sitemap-blog.xml``. The ``Sitemap`` +classes and the ``sitemaps`` dict don't change at all. + +Pinging Google +============== + +You may want to "ping" Google when your sitemap changes, to let it know to +reindex your site. The framework provides a function to do just that: +``django.contrib.sitemaps.ping_google()``. + +``ping_google()`` takes an optional argument, ``sitemap_url``, which should be +the absolute URL of your site's sitemap (e.g., ``'/sitemap.xml'``). If this +argument isn't provided, ``ping_google()`` will attempt to figure out your +sitemap by performing a reverse looking in your URLconf. + +``ping_google()`` raises the exception +``django.contrib.sitemaps.SitemapNotFound`` if it cannot determine your sitemap +URL. + +One useful way to call ``ping_google()`` is from a model's ``save()`` method:: + + from django.contrib.sitemaps import ping_google + + class Entry(models.Model): + # ... + def save(self): + super(Entry, self).save() + try: + ping_google() + except Exception: + # Bare 'except' because we could get a variety + # of HTTP-related exceptions. + pass + +A more efficient solution, however, would be to call ``ping_google()`` from a +cron script, or some other scheduled task. The function makes an HTTP request +to Google's servers, so you may not want to introduce that network overhead +each time you call ``save()``. diff --git a/docs/sites.txt b/docs/sites.txt index cca9f14f31..8c5f1fc64b 100644 --- a/docs/sites.txt +++ b/docs/sites.txt @@ -266,7 +266,18 @@ this:: If you attempt to use ``CurrentSiteManager`` and pass a field name that doesn't exist, Django will raise a ``ValueError``. +Finally, note that you'll probably want to keep a normal (non-site-specific) +``Manager`` on your model, even if you use ``CurrentSiteManager``. As explained +in the `manager documentation`_, if you define a manager manually, then Django +won't create the automatic ``objects = models.Manager()`` manager for you. +Also, note that certain parts of Django -- namely, the Django admin site and +generic views -- use whichever manager is defined *first* in the model, so if +you want your admin site to have access to all objects (not just site-specific +ones), put ``objects = models.Manager()`` in your model, before you define +``CurrentSiteManager``. + .. _manager: http://www.djangoproject.com/documentation/model_api/#managers +.. _manager documentation: http://www.djangoproject.com/documentation/model_api/#managers How Django uses the sites framework =================================== diff --git a/docs/templates_python.txt b/docs/templates_python.txt index 62069ffd6a..aa7fa901b0 100644 --- a/docs/templates_python.txt +++ b/docs/templates_python.txt @@ -300,13 +300,22 @@ If ``TEMPLATE_CONTEXT_PROCESSORS`` contains this processor, every * ``user`` -- An ``auth.User`` instance representing the currently logged-in user (or an ``AnonymousUser`` instance, if the client isn't logged in). See the `user authentication docs`. - * ``messages`` -- A list of ``auth.Message`` objects for the currently - logged-in user. - * ``perms`` -- An instance of ``django.core.context_processors.PermWrapper``, - representing the permissions that the currently logged-in user has. See - the `permissions docs`_. + + * ``messages`` -- A list of messages (as strings) for the currently + logged-in user. Behind the scenes, this calls + ``request.user.get_and_delete_messages()`` for every request. That method + collects the user's messages and deletes them from the database. + + Note that messages are set with ``user.add_message()``. See the + `message docs`_ for more. + + * ``perms`` -- An instance of + ``django.core.context_processors.PermWrapper``, representing the + permissions that the currently logged-in user has. See the `permissions + docs`_. .. _user authentication docs: http://www.djangoproject.com/documentation/authentication/#users +.. _message docs: http://www.djangoproject.com/documentation/authentication/#messages .. _permissions docs: http://www.djangoproject.com/documentation/authentication/#permissions django.core.context_processors.debug diff --git a/docs/testing.txt b/docs/testing.txt new file mode 100644 index 0000000000..98ed1e8aec --- /dev/null +++ b/docs/testing.txt @@ -0,0 +1,303 @@ +=========================== +Testing Django applications +=========================== + +**New in Django development version**. + +Automated testing is an extremely useful weapon in the bug-killing arsenal +of the modern developer. When initially writing code, a test suite can be +used to validate that code behaves as expected. When refactoring or +modifying code, tests serve as a guide to ensure that behavior hasn't +changed unexpectedly as a result of the refactor. + +Testing an web application is a complex task, as there are many +components of a web application that must be validated and tested. To +help you test your application, Django provides a test execution +framework, and range of utilities that can be used to stimulate and +inspect various facets of a web application. + + This testing framework is currently under development, and may change + slightly before the next official Django release. + + (That's *no* excuse not to write tests, though!) + +Writing tests +============= + +Tests in Django come in two forms: doctests and unit tests. + +Writing doctests +---------------- + +Doctests use Python's standard doctest_ module, which searches for tests in +your docstrings. Django's test runner looks for doctests in your ``models.py`` +file, and executes any that it finds. Django will also search for a file +called ``tests.py`` in the application directory (i.e., the directory that +holds ``models.py``). If a ``tests.py`` is found, it will also be searched +for doctests. + +.. admonition:: What's a **docstring**? + + A good explanation of docstrings (and some guidlines for using them + effectively) can be found in :PEP:`257`: + + A docstring is a string literal that occurs as the first statement in + a module, function, class, or method definition. Such a docstring + becomes the ``__doc__`` special attribute of that object. + + Since tests often make great documentation, doctest lets you put your + tests directly in your docstrings. + +You can put doctest strings on any object in your ``models.py``, but it's +common practice to put application-level doctests in the module docstring, and +model-level doctests in the docstring for each model. + +For example:: + + from django.db import model + + class Animal(models.Model): + """ + An animal that knows how to make noise + + # Create some animals + >>> lion = Animal.objects.create(name="lion", sound="roar") + >>> cat = Animal.objects.create(name="cat", sound="meow") + + # Make 'em speak + >>> lion.speak() + 'The lion says "roar"' + >>> cat.speak() + 'The cat says "meow"' + """ + + name = models.CharField(maxlength=20) + sound = models.CharField(maxlength=20) + + def speak(self): + return 'The %s says "%s"' % (self.name, self.sound) + +When you `run your tests`_, the test utility will find this docstring, notice +that portions of it look like an interactive Python session, and execute those +lines while checking that the results match. + +For more details about how doctest works, see the `standard library +documentation for doctest`_ + +.. _doctest: http://docs.python.org/lib/module-doctest.html +.. _standard library documentation for doctest: doctest_ + +Writing unittests +----------------- + +Like doctests, Django's unit tests use a standard library module: unittest_. +As with doctests, Django's test runner looks for any unit test cases defined +in ``models.py``, or in a ``tests.py`` file in your application directory. + +An equivalent unittest test case for the above example would look like:: + + import unittest + from myapp.models import Animal + + class AnimalTestCase(unittest.TestCase): + + def setUp(self): + self.lion = Animal.objects.create(name="lion", sound="roar") + self.cat = Animal.objects.create(name="cat", sound="meow") + + def testSpeaking(self): + self.assertEquals(self.lion.speak(), 'The lion says "roar"') + self.assertEquals(self.cat.speak(), 'The cat says "meow"') + +When you `run your tests`_, the test utility will find all the test cases +(that is, subclasses of ``unittest.TestCase``) in ``tests.py``, automatically +build a test suite out of those test cases, and run that suite. + +For more details about ``unittest``, see the `standard library unittest +documentation`_. + +.. _unittest: http://docs.python.org/lib/module-unittest.html +.. _standard library unittest documentation: unittest_ +.. _run your tests: `Running tests`_ + +Which should I use? +------------------- + +Choosing a test framework is often contentious, so Django simply supports +both of the standard Python test frameworks. Choosing one is up to each +developer's personal tastes; each is supported equally. Since each test +system has different benefits, the best approach is probably to use both +together, picking the test system to match the type of tests you need to +write. + +For developers new to testing, however, this choice can seem +confusing, so here are a few key differences to help you decide weather +doctests or unit tests are right for you. + +If you've been using Python for a while, ``doctest`` will probably feel more +"pythonic". It's designed to make writing tests as easy as possible, so +there's no overhead of writing classes or methods; you simply put tests in +docstrings. This gives the added advantage of given your modules automatic +documentation -- well-written doctests can kill both the documentation and the +testing bird with a single stone. + +For developers just getting started with testing, using doctests will probably +get you started faster. + +The ``unittest`` framework will probably feel very familiar to developers +coming from Java. Since ``unittest`` is inspired by Java's JUnit, if +you've used testing frameworks in other languages that similarly were +inspired by JUnit, ``unittest`` should also feel pretty familiar. + +Since ``unittest`` is organized around classes and methods, if you need +to write a bunch of tests that all share similar code, you can easily use +subclass to abstract common tasks; this makes test code shorter and cleaner. +There's also support for explicit setup and/or cleanup routines, which give +you a high level of control over the environment your test cases run in. + +Again, remember that you can use both systems side-by-side (even in the same +app). In the end, most projects will eventually end up using both; each shines +in different circumstances. + +Testing utilities +================= + +Test Client +----------- + +A dummy browser; instruments the template generation process... + +Fixtures +-------- + +Feature still to come... + + +Running tests +============= + +Run your tests using your project's ``manage.py`` utility:: + + $ ./manage.py test + +If you only want to run tests for a particular application, add the +application name to the command line. For example, if your +``INSTALLED_APPS`` contains ``myproject.polls`` and ``myproject.animals``, +but you only want to run the animals unit tests, run:: + + $ ./manage.py test animals + +When you run your tests, you'll see a bunch of text flow by as the test +database is created and models are initialized. This test database is +created from scratch every time you run your tests. + +By default, the test database gets its name by prepending ``test_`` to +the database name specified by the ``DATABASE_NAME`` setting; all other +database settings will the same as they would be for the project normally. +If you wish to use a name other than the default for the test database, +you can use the ``TEST_DATABASE_NAME`` setting to provide a name. + +Once the test database has been established, Django will run your tests. +If everything goes well, at the end you'll see:: + + ---------------------------------------------------------------------- + Ran 22 tests in 0.221s + + OK + +If there are test failures, however, you'll see full details about what tests +failed:: + + ====================================================================== + FAIL: Doctest: ellington.core.throttle.models + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "/dev/django/test/doctest.py", line 2153, in runTest + raise self.failureException(self.format_failure(new.getvalue())) + AssertionError: Failed doctest test for myapp.models + File "/dev/myapp/models.py", line 0, in models + + ---------------------------------------------------------------------- + File "/dev/myapp/models.py", line 14, in myapp.models + Failed example: + throttle.check("actor A", "action one", limit=2, hours=1) + Expected: + True + Got: + False + + ---------------------------------------------------------------------- + Ran 2 tests in 0.048s + + FAILED (failures=1) + +When the tests have all been executed, the test database is destroyed. + +Using a different testing framework +=================================== + +Doctest and Unittest are not the only Python testing frameworks. While +Django doesn't provide explicit support these alternative frameworks, +it does provide a mechanism to allow you to invoke tests constructed for +an alternative framework as if they were normal Django tests. + +When you run ``./manage.py test``, Django looks at the ``TEST_RUNNER`` +setting to determine what to do. By default, ``TEST_RUNNER`` points to ``django.test.simple.run_tests``. This method defines the default Django +testing behaviour. This behaviour involves: + +#. Creating the test database +#. Running ``syncdb`` to install models and initial data into the test database +#. Looking for Unit Tests and Doctests in ``models.py`` and ``tests.py`` file for each installed application +#. Running the Unit Tests and Doctests that are found +#. Destroying the test database. + +If you define your own test runner method and point ``TEST_RUNNER`` +at that method, Django will execute your test runner whenever you run +``./manage.py test``. In this way, it is possible to use any test +framework that can be executed from Python code. + +Defining a test runner +---------------------- +By convention, a test runner should be called ``run_tests``; however, you +can call it anything you want. The only requirement is that it accept two +arguments: + +``run_tests(module_list, verbosity=1)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The module list is the list of Python modules that contain the models to be +tested. This is the same format returned by ``django.db.models.get_apps()`` + +Verbosity determines the amount of notification and debug information that +will be printed to the console; '0' is no output, '1' is normal output, +and `2` is verbose output. + +Testing utilities +----------------- + +To assist in the creation of your own test runner, Django provides +a number of utility methods in the ``django.test.utils`` module. + +``create_test_db(verbosity=1, autoclobber=False)``: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Creates a new test database, and run ``syncdb`` against it. + +``verbosity`` has the same behaviour as in the test runner. + +``Autoclobber`` describes the behavior that will occur if a database with +the same name as the test database is discovered. If ``autoclobber`` is False, +the user will be asked to approve destroying the existing database. ``sys.exit`` +is called if the user does not approve. If autoclobber is ``True``, the database +will be destroyed without consulting the user. + +``create_test_db()`` has the side effect of modifying +``settings.DATABASE_NAME`` to match the name of the test database. + +``destroy_test_db(old_database_name, verbosity=1)``: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Destroys the database with the name ``settings.DATABASE_NAME`` matching, +and restores the value of ``settings.DATABASE_NAME`` to the provided name. + +``verbosity`` has the same behaviour as in the test runner. diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000..d3d908abf5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[bdist_rpm] +doc_files = docs/*.txt + diff --git a/tests/doctest.py b/tests/doctest.py deleted file mode 100644 index df7aa978d3..0000000000 --- a/tests/doctest.py +++ /dev/null @@ -1,2665 +0,0 @@ -# Module doctest. -# Released to the public domain 16-Jan-2001, by Tim Peters (tim@python.org). -# Major enhancements and refactoring by: -# Jim Fulton -# Edward Loper - -# Provided as-is; use at your own risk; no warranty; no promises; enjoy! - -r"""Module doctest -- a framework for running examples in docstrings. - -In simplest use, end each module M to be tested with: - -def _test(): - import doctest - doctest.testmod() - -if __name__ == "__main__": - _test() - -Then running the module as a script will cause the examples in the -docstrings to get executed and verified: - -python M.py - -This won't display anything unless an example fails, in which case the -failing example(s) and the cause(s) of the failure(s) are printed to stdout -(why not stderr? because stderr is a lame hack <0.2 wink>), and the final -line of output is "Test failed.". - -Run it with the -v switch instead: - -python M.py -v - -and a detailed report of all examples tried is printed to stdout, along -with assorted summaries at the end. - -You can force verbose mode by passing "verbose=True" to testmod, or prohibit -it by passing "verbose=False". In either of those cases, sys.argv is not -examined by testmod. - -There are a variety of other ways to run doctests, including integration -with the unittest framework, and support for running non-Python text -files containing doctests. There are also many ways to override parts -of doctest's default behaviors. See the Library Reference Manual for -details. -""" - -__docformat__ = 'reStructuredText en' - -__all__ = [ - # 0, Option Flags - 'register_optionflag', - 'DONT_ACCEPT_TRUE_FOR_1', - 'DONT_ACCEPT_BLANKLINE', - 'NORMALIZE_WHITESPACE', - 'ELLIPSIS', - 'IGNORE_EXCEPTION_DETAIL', - 'COMPARISON_FLAGS', - 'REPORT_UDIFF', - 'REPORT_CDIFF', - 'REPORT_NDIFF', - 'REPORT_ONLY_FIRST_FAILURE', - 'REPORTING_FLAGS', - # 1. Utility Functions - 'is_private', - # 2. Example & DocTest - 'Example', - 'DocTest', - # 3. Doctest Parser - 'DocTestParser', - # 4. Doctest Finder - 'DocTestFinder', - # 5. Doctest Runner - 'DocTestRunner', - 'OutputChecker', - 'DocTestFailure', - 'UnexpectedException', - 'DebugRunner', - # 6. Test Functions - 'testmod', - 'testfile', - 'run_docstring_examples', - # 7. Tester - 'Tester', - # 8. Unittest Support - 'DocTestSuite', - 'DocFileSuite', - 'set_unittest_reportflags', - # 9. Debugging Support - 'script_from_examples', - 'testsource', - 'debug_src', - 'debug', -] - -import __future__ - -import sys, traceback, inspect, linecache, os, re, types -import unittest, difflib, pdb, tempfile -import warnings -from StringIO import StringIO - -# Don't whine about the deprecated is_private function in this -# module's tests. -warnings.filterwarnings("ignore", "is_private", DeprecationWarning, - __name__, 0) - -# There are 4 basic classes: -# - Example: a pair, plus an intra-docstring line number. -# - DocTest: a collection of examples, parsed from a docstring, plus -# info about where the docstring came from (name, filename, lineno). -# - DocTestFinder: extracts DocTests from a given object's docstring and -# its contained objects' docstrings. -# - DocTestRunner: runs DocTest cases, and accumulates statistics. -# -# So the basic picture is: -# -# list of: -# +------+ +---------+ +-------+ -# |object| --DocTestFinder-> | DocTest | --DocTestRunner-> |results| -# +------+ +---------+ +-------+ -# | Example | -# | ... | -# | Example | -# +---------+ - -# Option constants. - -OPTIONFLAGS_BY_NAME = {} -def register_optionflag(name): - flag = 1 << len(OPTIONFLAGS_BY_NAME) - OPTIONFLAGS_BY_NAME[name] = flag - return flag - -DONT_ACCEPT_TRUE_FOR_1 = register_optionflag('DONT_ACCEPT_TRUE_FOR_1') -DONT_ACCEPT_BLANKLINE = register_optionflag('DONT_ACCEPT_BLANKLINE') -NORMALIZE_WHITESPACE = register_optionflag('NORMALIZE_WHITESPACE') -ELLIPSIS = register_optionflag('ELLIPSIS') -IGNORE_EXCEPTION_DETAIL = register_optionflag('IGNORE_EXCEPTION_DETAIL') - -COMPARISON_FLAGS = (DONT_ACCEPT_TRUE_FOR_1 | - DONT_ACCEPT_BLANKLINE | - NORMALIZE_WHITESPACE | - ELLIPSIS | - IGNORE_EXCEPTION_DETAIL) - -REPORT_UDIFF = register_optionflag('REPORT_UDIFF') -REPORT_CDIFF = register_optionflag('REPORT_CDIFF') -REPORT_NDIFF = register_optionflag('REPORT_NDIFF') -REPORT_ONLY_FIRST_FAILURE = register_optionflag('REPORT_ONLY_FIRST_FAILURE') - -REPORTING_FLAGS = (REPORT_UDIFF | - REPORT_CDIFF | - REPORT_NDIFF | - REPORT_ONLY_FIRST_FAILURE) - -# Special string markers for use in `want` strings: -BLANKLINE_MARKER = '' -ELLIPSIS_MARKER = '...' - -###################################################################### -## Table of Contents -###################################################################### -# 1. Utility Functions -# 2. Example & DocTest -- store test cases -# 3. DocTest Parser -- extracts examples from strings -# 4. DocTest Finder -- extracts test cases from objects -# 5. DocTest Runner -- runs test cases -# 6. Test Functions -- convenient wrappers for testing -# 7. Tester Class -- for backwards compatibility -# 8. Unittest Support -# 9. Debugging Support -# 10. Example Usage - -###################################################################### -## 1. Utility Functions -###################################################################### - -def is_private(prefix, base): - """prefix, base -> true iff name prefix + "." + base is "private". - - Prefix may be an empty string, and base does not contain a period. - Prefix is ignored (although functions you write conforming to this - protocol may make use of it). - Return true iff base begins with an (at least one) underscore, but - does not both begin and end with (at least) two underscores. - - >>> is_private("a.b", "my_func") - False - >>> is_private("____", "_my_func") - True - >>> is_private("someclass", "__init__") - False - >>> is_private("sometypo", "__init_") - True - >>> is_private("x.y.z", "_") - True - >>> is_private("_x.y.z", "__") - False - >>> is_private("", "") # senseless but consistent - False - """ - warnings.warn("is_private is deprecated; it wasn't useful; " - "examine DocTestFinder.find() lists instead", - DeprecationWarning, stacklevel=2) - return base[:1] == "_" and not base[:2] == "__" == base[-2:] - -def _extract_future_flags(globs): - """ - Return the compiler-flags associated with the future features that - have been imported into the given namespace (globs). - """ - flags = 0 - for fname in __future__.all_feature_names: - feature = globs.get(fname, None) - if feature is getattr(__future__, fname): - flags |= feature.compiler_flag - return flags - -def _normalize_module(module, depth=2): - """ - Return the module specified by `module`. In particular: - - If `module` is a module, then return module. - - If `module` is a string, then import and return the - module with that name. - - If `module` is None, then return the calling module. - The calling module is assumed to be the module of - the stack frame at the given depth in the call stack. - """ - if inspect.ismodule(module): - return module - elif isinstance(module, (str, unicode)): - return __import__(module, globals(), locals(), ["*"]) - elif module is None: - return sys.modules[sys._getframe(depth).f_globals['__name__']] - else: - raise TypeError("Expected a module, string, or None") - -def _indent(s, indent=4): - """ - Add the given number of space characters to the beginning every - non-blank line in `s`, and return the result. - """ - # This regexp matches the start of non-blank lines: - return re.sub('(?m)^(?!$)', indent*' ', s) - -def _exception_traceback(exc_info): - """ - Return a string containing a traceback message for the given - exc_info tuple (as returned by sys.exc_info()). - """ - # Get a traceback message. - excout = StringIO() - exc_type, exc_val, exc_tb = exc_info - traceback.print_exception(exc_type, exc_val, exc_tb, file=excout) - return excout.getvalue() - -# Override some StringIO methods. -class _SpoofOut(StringIO): - def getvalue(self): - result = StringIO.getvalue(self) - # If anything at all was written, make sure there's a trailing - # newline. There's no way for the expected output to indicate - # that a trailing newline is missing. - if result and not result.endswith("\n"): - result += "\n" - # Prevent softspace from screwing up the next test case, in - # case they used print with a trailing comma in an example. - if hasattr(self, "softspace"): - del self.softspace - return result - - def truncate(self, size=None): - StringIO.truncate(self, size) - if hasattr(self, "softspace"): - del self.softspace - -# Worst-case linear-time ellipsis matching. -def _ellipsis_match(want, got): - """ - Essentially the only subtle case: - >>> _ellipsis_match('aa...aa', 'aaa') - False - """ - if ELLIPSIS_MARKER not in want: - return want == got - - # Find "the real" strings. - ws = want.split(ELLIPSIS_MARKER) - assert len(ws) >= 2 - - # Deal with exact matches possibly needed at one or both ends. - startpos, endpos = 0, len(got) - w = ws[0] - if w: # starts with exact match - if got.startswith(w): - startpos = len(w) - del ws[0] - else: - return False - w = ws[-1] - if w: # ends with exact match - if got.endswith(w): - endpos -= len(w) - del ws[-1] - else: - return False - - if startpos > endpos: - # Exact end matches required more characters than we have, as in - # _ellipsis_match('aa...aa', 'aaa') - return False - - # For the rest, we only need to find the leftmost non-overlapping - # match for each piece. If there's no overall match that way alone, - # there's no overall match period. - for w in ws: - # w may be '' at times, if there are consecutive ellipses, or - # due to an ellipsis at the start or end of `want`. That's OK. - # Search for an empty string succeeds, and doesn't change startpos. - startpos = got.find(w, startpos, endpos) - if startpos < 0: - return False - startpos += len(w) - - return True - -def _comment_line(line): - "Return a commented form of the given line" - line = line.rstrip() - if line: - return '# '+line - else: - return '#' - -class _OutputRedirectingPdb(pdb.Pdb): - """ - A specialized version of the python debugger that redirects stdout - to a given stream when interacting with the user. Stdout is *not* - redirected when traced code is executed. - """ - def __init__(self, out): - self.__out = out - pdb.Pdb.__init__(self) - - def trace_dispatch(self, *args): - # Redirect stdout to the given stream. - save_stdout = sys.stdout - sys.stdout = self.__out - # Call Pdb's trace dispatch method. - try: - return pdb.Pdb.trace_dispatch(self, *args) - finally: - sys.stdout = save_stdout - -# [XX] Normalize with respect to os.path.pardir? -def _module_relative_path(module, path): - if not inspect.ismodule(module): - raise TypeError, 'Expected a module: %r' % module - if path.startswith('/'): - raise ValueError, 'Module-relative files may not have absolute paths' - - # Find the base directory for the path. - if hasattr(module, '__file__'): - # A normal module/package - basedir = os.path.split(module.__file__)[0] - elif module.__name__ == '__main__': - # An interactive session. - if len(sys.argv)>0 and sys.argv[0] != '': - basedir = os.path.split(sys.argv[0])[0] - else: - basedir = os.curdir - else: - # A module w/o __file__ (this includes builtins) - raise ValueError("Can't resolve paths relative to the module " + - module + " (it has no __file__)") - - # Combine the base directory and the path. - return os.path.join(basedir, *(path.split('/'))) - -###################################################################### -## 2. Example & DocTest -###################################################################### -## - An "example" is a pair, where "source" is a -## fragment of source code, and "want" is the expected output for -## "source." The Example class also includes information about -## where the example was extracted from. -## -## - A "doctest" is a collection of examples, typically extracted from -## a string (such as an object's docstring). The DocTest class also -## includes information about where the string was extracted from. - -class Example: - """ - A single doctest example, consisting of source code and expected - output. `Example` defines the following attributes: - - - source: A single Python statement, always ending with a newline. - The constructor adds a newline if needed. - - - want: The expected output from running the source code (either - from stdout, or a traceback in case of exception). `want` ends - with a newline unless it's empty, in which case it's an empty - string. The constructor adds a newline if needed. - - - exc_msg: The exception message generated by the example, if - the example is expected to generate an exception; or `None` if - it is not expected to generate an exception. This exception - message is compared against the return value of - `traceback.format_exception_only()`. `exc_msg` ends with a - newline unless it's `None`. The constructor adds a newline - if needed. - - - lineno: The line number within the DocTest string containing - this Example where the Example begins. This line number is - zero-based, with respect to the beginning of the DocTest. - - - indent: The example's indentation in the DocTest string. - I.e., the number of space characters that preceed the - example's first prompt. - - - options: A dictionary mapping from option flags to True or - False, which is used to override default options for this - example. Any option flags not contained in this dictionary - are left at their default value (as specified by the - DocTestRunner's optionflags). By default, no options are set. - """ - def __init__(self, source, want, exc_msg=None, lineno=0, indent=0, - options=None): - # Normalize inputs. - if not source.endswith('\n'): - source += '\n' - if want and not want.endswith('\n'): - want += '\n' - if exc_msg is not None and not exc_msg.endswith('\n'): - exc_msg += '\n' - # Store properties. - self.source = source - self.want = want - self.lineno = lineno - self.indent = indent - if options is None: options = {} - self.options = options - self.exc_msg = exc_msg - -class DocTest: - """ - A collection of doctest examples that should be run in a single - namespace. Each `DocTest` defines the following attributes: - - - examples: the list of examples. - - - globs: The namespace (aka globals) that the examples should - be run in. - - - name: A name identifying the DocTest (typically, the name of - the object whose docstring this DocTest was extracted from). - - - filename: The name of the file that this DocTest was extracted - from, or `None` if the filename is unknown. - - - lineno: The line number within filename where this DocTest - begins, or `None` if the line number is unavailable. This - line number is zero-based, with respect to the beginning of - the file. - - - docstring: The string that the examples were extracted from, - or `None` if the string is unavailable. - """ - def __init__(self, examples, globs, name, filename, lineno, docstring): - """ - Create a new DocTest containing the given examples. The - DocTest's globals are initialized with a copy of `globs`. - """ - assert not isinstance(examples, basestring), \ - "DocTest no longer accepts str; use DocTestParser instead" - self.examples = examples - self.docstring = docstring - self.globs = globs.copy() - self.name = name - self.filename = filename - self.lineno = lineno - - def __repr__(self): - if len(self.examples) == 0: - examples = 'no examples' - elif len(self.examples) == 1: - examples = '1 example' - else: - examples = '%d examples' % len(self.examples) - return ('' % - (self.name, self.filename, self.lineno, examples)) - - - # This lets us sort tests by name: - def __cmp__(self, other): - if not isinstance(other, DocTest): - return -1 - return cmp((self.name, self.filename, self.lineno, id(self)), - (other.name, other.filename, other.lineno, id(other))) - -###################################################################### -## 3. DocTestParser -###################################################################### - -class DocTestParser: - """ - A class used to parse strings containing doctest examples. - """ - # This regular expression is used to find doctest examples in a - # string. It defines three groups: `source` is the source code - # (including leading indentation and prompts); `indent` is the - # indentation of the first (PS1) line of the source code; and - # `want` is the expected output (including leading indentation). - _EXAMPLE_RE = re.compile(r''' - # Source consists of a PS1 line followed by zero or more PS2 lines. - (?P - (?:^(?P [ ]*) >>> .*) # PS1 line - (?:\n [ ]* \.\.\. .*)*) # PS2 lines - \n? - # Want consists of any non-blank lines that do not start with PS1. - (?P (?:(?![ ]*$) # Not a blank line - (?![ ]*>>>) # Not a line starting with PS1 - .*$\n? # But any other line - )*) - ''', re.MULTILINE | re.VERBOSE) - - # A regular expression for handling `want` strings that contain - # expected exceptions. It divides `want` into three pieces: - # - the traceback header line (`hdr`) - # - the traceback stack (`stack`) - # - the exception message (`msg`), as generated by - # traceback.format_exception_only() - # `msg` may have multiple lines. We assume/require that the - # exception message is the first non-indented line starting with a word - # character following the traceback header line. - _EXCEPTION_RE = re.compile(r""" - # Grab the traceback header. Different versions of Python have - # said different things on the first traceback line. - ^(?P Traceback\ \( - (?: most\ recent\ call\ last - | innermost\ last - ) \) : - ) - \s* $ # toss trailing whitespace on the header. - (?P .*?) # don't blink: absorb stuff until... - ^ (?P \w+ .*) # a line *starts* with alphanum. - """, re.VERBOSE | re.MULTILINE | re.DOTALL) - - # A callable returning a true value iff its argument is a blank line - # or contains a single comment. - _IS_BLANK_OR_COMMENT = re.compile(r'^[ ]*(#.*)?$').match - - def parse(self, string, name=''): - """ - Divide the given string into examples and intervening text, - and return them as a list of alternating Examples and strings. - Line numbers for the Examples are 0-based. The optional - argument `name` is a name identifying this string, and is only - used for error messages. - """ - string = string.expandtabs() - # If all lines begin with the same indentation, then strip it. - min_indent = self._min_indent(string) - if min_indent > 0: - string = '\n'.join([l[min_indent:] for l in string.split('\n')]) - - output = [] - charno, lineno = 0, 0 - # Find all doctest examples in the string: - for m in self._EXAMPLE_RE.finditer(string): - # Add the pre-example text to `output`. - output.append(string[charno:m.start()]) - # Update lineno (lines before this example) - lineno += string.count('\n', charno, m.start()) - # Extract info from the regexp match. - (source, options, want, exc_msg) = \ - self._parse_example(m, name, lineno) - # Create an Example, and add it to the list. - if not self._IS_BLANK_OR_COMMENT(source): - output.append( Example(source, want, exc_msg, - lineno=lineno, - indent=min_indent+len(m.group('indent')), - options=options) ) - # Update lineno (lines inside this example) - lineno += string.count('\n', m.start(), m.end()) - # Update charno. - charno = m.end() - # Add any remaining post-example text to `output`. - output.append(string[charno:]) - return output - - def get_doctest(self, string, globs, name, filename, lineno): - """ - Extract all doctest examples from the given string, and - collect them into a `DocTest` object. - - `globs`, `name`, `filename`, and `lineno` are attributes for - the new `DocTest` object. See the documentation for `DocTest` - for more information. - """ - return DocTest(self.get_examples(string, name), globs, - name, filename, lineno, string) - - def get_examples(self, string, name=''): - """ - Extract all doctest examples from the given string, and return - them as a list of `Example` objects. Line numbers are - 0-based, because it's most common in doctests that nothing - interesting appears on the same line as opening triple-quote, - and so the first interesting line is called \"line 1\" then. - - The optional argument `name` is a name identifying this - string, and is only used for error messages. - """ - return [x for x in self.parse(string, name) - if isinstance(x, Example)] - - def _parse_example(self, m, name, lineno): - """ - Given a regular expression match from `_EXAMPLE_RE` (`m`), - return a pair `(source, want)`, where `source` is the matched - example's source code (with prompts and indentation stripped); - and `want` is the example's expected output (with indentation - stripped). - - `name` is the string's name, and `lineno` is the line number - where the example starts; both are used for error messages. - """ - # Get the example's indentation level. - indent = len(m.group('indent')) - - # Divide source into lines; check that they're properly - # indented; and then strip their indentation & prompts. - source_lines = m.group('source').split('\n') - self._check_prompt_blank(source_lines, indent, name, lineno) - self._check_prefix(source_lines[1:], ' '*indent + '.', name, lineno) - source = '\n'.join([sl[indent+4:] for sl in source_lines]) - - # Divide want into lines; check that it's properly indented; and - # then strip the indentation. Spaces before the last newline should - # be preserved, so plain rstrip() isn't good enough. - want = m.group('want') - want_lines = want.split('\n') - if len(want_lines) > 1 and re.match(r' *$', want_lines[-1]): - del want_lines[-1] # forget final newline & spaces after it - self._check_prefix(want_lines, ' '*indent, name, - lineno + len(source_lines)) - want = '\n'.join([wl[indent:] for wl in want_lines]) - - # If `want` contains a traceback message, then extract it. - m = self._EXCEPTION_RE.match(want) - if m: - exc_msg = m.group('msg') - else: - exc_msg = None - - # Extract options from the source. - options = self._find_options(source, name, lineno) - - return source, options, want, exc_msg - - # This regular expression looks for option directives in the - # source code of an example. Option directives are comments - # starting with "doctest:". Warning: this may give false - # positives for string-literals that contain the string - # "#doctest:". Eliminating these false positives would require - # actually parsing the string; but we limit them by ignoring any - # line containing "#doctest:" that is *followed* by a quote mark. - _OPTION_DIRECTIVE_RE = re.compile(r'#\s*doctest:\s*([^\n\'"]*)$', - re.MULTILINE) - - def _find_options(self, source, name, lineno): - """ - Return a dictionary containing option overrides extracted from - option directives in the given source string. - - `name` is the string's name, and `lineno` is the line number - where the example starts; both are used for error messages. - """ - options = {} - # (note: with the current regexp, this will match at most once:) - for m in self._OPTION_DIRECTIVE_RE.finditer(source): - option_strings = m.group(1).replace(',', ' ').split() - for option in option_strings: - if (option[0] not in '+-' or - option[1:] not in OPTIONFLAGS_BY_NAME): - raise ValueError('line %r of the doctest for %s ' - 'has an invalid option: %r' % - (lineno+1, name, option)) - flag = OPTIONFLAGS_BY_NAME[option[1:]] - options[flag] = (option[0] == '+') - if options and self._IS_BLANK_OR_COMMENT(source): - raise ValueError('line %r of the doctest for %s has an option ' - 'directive on a line with no example: %r' % - (lineno, name, source)) - return options - - # This regular expression finds the indentation of every non-blank - # line in a string. - _INDENT_RE = re.compile('^([ ]*)(?=\S)', re.MULTILINE) - - def _min_indent(self, s): - "Return the minimum indentation of any non-blank line in `s`" - indents = [len(indent) for indent in self._INDENT_RE.findall(s)] - if len(indents) > 0: - return min(indents) - else: - return 0 - - def _check_prompt_blank(self, lines, indent, name, lineno): - """ - Given the lines of a source string (including prompts and - leading indentation), check to make sure that every prompt is - followed by a space character. If any line is not followed by - a space character, then raise ValueError. - """ - for i, line in enumerate(lines): - if len(line) >= indent+4 and line[indent+3] != ' ': - raise ValueError('line %r of the docstring for %s ' - 'lacks blank after %s: %r' % - (lineno+i+1, name, - line[indent:indent+3], line)) - - def _check_prefix(self, lines, prefix, name, lineno): - """ - Check that every line in the given list starts with the given - prefix; if any line does not, then raise a ValueError. - """ - for i, line in enumerate(lines): - if line and not line.startswith(prefix): - raise ValueError('line %r of the docstring for %s has ' - 'inconsistent leading whitespace: %r' % - (lineno+i+1, name, line)) - - -###################################################################### -## 4. DocTest Finder -###################################################################### - -class DocTestFinder: - """ - A class used to extract the DocTests that are relevant to a given - object, from its docstring and the docstrings of its contained - objects. Doctests can currently be extracted from the following - object types: modules, functions, classes, methods, staticmethods, - classmethods, and properties. - """ - - def __init__(self, verbose=False, parser=DocTestParser(), - recurse=True, _namefilter=None, exclude_empty=True): - """ - Create a new doctest finder. - - The optional argument `parser` specifies a class or - function that should be used to create new DocTest objects (or - objects that implement the same interface as DocTest). The - signature for this factory function should match the signature - of the DocTest constructor. - - If the optional argument `recurse` is false, then `find` will - only examine the given object, and not any contained objects. - - If the optional argument `exclude_empty` is false, then `find` - will include tests for objects with empty docstrings. - """ - self._parser = parser - self._verbose = verbose - self._recurse = recurse - self._exclude_empty = exclude_empty - # _namefilter is undocumented, and exists only for temporary backward- - # compatibility support of testmod's deprecated isprivate mess. - self._namefilter = _namefilter - - def find(self, obj, name=None, module=None, globs=None, - extraglobs=None): - """ - Return a list of the DocTests that are defined by the given - object's docstring, or by any of its contained objects' - docstrings. - - The optional parameter `module` is the module that contains - the given object. If the module is not specified or is None, then - the test finder will attempt to automatically determine the - correct module. The object's module is used: - - - As a default namespace, if `globs` is not specified. - - To prevent the DocTestFinder from extracting DocTests - from objects that are imported from other modules. - - To find the name of the file containing the object. - - To help find the line number of the object within its - file. - - Contained objects whose module does not match `module` are ignored. - - If `module` is False, no attempt to find the module will be made. - This is obscure, of use mostly in tests: if `module` is False, or - is None but cannot be found automatically, then all objects are - considered to belong to the (non-existent) module, so all contained - objects will (recursively) be searched for doctests. - - The globals for each DocTest is formed by combining `globs` - and `extraglobs` (bindings in `extraglobs` override bindings - in `globs`). A new copy of the globals dictionary is created - for each DocTest. If `globs` is not specified, then it - defaults to the module's `__dict__`, if specified, or {} - otherwise. If `extraglobs` is not specified, then it defaults - to {}. - - """ - # If name was not specified, then extract it from the object. - if name is None: - name = getattr(obj, '__name__', None) - if name is None: - raise ValueError("DocTestFinder.find: name must be given " - "when obj.__name__ doesn't exist: %r" % - (type(obj),)) - - # Find the module that contains the given object (if obj is - # a module, then module=obj.). Note: this may fail, in which - # case module will be None. - if module is False: - module = None - elif module is None: - module = inspect.getmodule(obj) - - # Read the module's source code. This is used by - # DocTestFinder._find_lineno to find the line number for a - # given object's docstring. - try: - file = inspect.getsourcefile(obj) or inspect.getfile(obj) - source_lines = linecache.getlines(file) - if not source_lines: - source_lines = None - except TypeError: - source_lines = None - - # Initialize globals, and merge in extraglobs. - if globs is None: - if module is None: - globs = {} - else: - globs = module.__dict__.copy() - else: - globs = globs.copy() - if extraglobs is not None: - globs.update(extraglobs) - - # Recursively explore `obj`, extracting DocTests. - tests = [] - self._find(tests, obj, name, module, source_lines, globs, {}) - return tests - - def _filter(self, obj, prefix, base): - """ - Return true if the given object should not be examined. - """ - return (self._namefilter is not None and - self._namefilter(prefix, base)) - - def _from_module(self, module, object): - """ - Return true if the given object is defined in the given - module. - """ - if module is None: - return True - elif inspect.isfunction(object): - return module.__dict__ is object.func_globals - elif inspect.isclass(object): - return module.__name__ == object.__module__ - elif inspect.getmodule(object) is not None: - return module is inspect.getmodule(object) - elif hasattr(object, '__module__'): - return module.__name__ == object.__module__ - elif isinstance(object, property): - return True # [XX] no way not be sure. - else: - raise ValueError("object must be a class or function") - - def _find(self, tests, obj, name, module, source_lines, globs, seen): - """ - Find tests for the given object and any contained objects, and - add them to `tests`. - """ - if self._verbose: - print 'Finding tests in %s' % name - - # If we've already processed this object, then ignore it. - if id(obj) in seen: - return - seen[id(obj)] = 1 - - # Find a test for this object, and add it to the list of tests. - test = self._get_test(obj, name, module, globs, source_lines) - if test is not None: - tests.append(test) - - # Look for tests in a module's contained objects. - if inspect.ismodule(obj) and self._recurse: - for valname, val in obj.__dict__.items(): - # Check if this contained object should be ignored. - if self._filter(val, name, valname): - continue - valname = '%s.%s' % (name, valname) - # Recurse to functions & classes. - if ((inspect.isfunction(val) or inspect.isclass(val)) and - self._from_module(module, val)): - self._find(tests, val, valname, module, source_lines, - globs, seen) - - # Look for tests in a module's __test__ dictionary. - if inspect.ismodule(obj) and self._recurse: - for valname, val in getattr(obj, '__test__', {}).items(): - if not isinstance(valname, basestring): - raise ValueError("DocTestFinder.find: __test__ keys " - "must be strings: %r" % - (type(valname),)) - if not (inspect.isfunction(val) or inspect.isclass(val) or - inspect.ismethod(val) or inspect.ismodule(val) or - isinstance(val, basestring)): - raise ValueError("DocTestFinder.find: __test__ values " - "must be strings, functions, methods, " - "classes, or modules: %r" % - (type(val),)) - valname = '%s.__test__.%s' % (name, valname) - self._find(tests, val, valname, module, source_lines, - globs, seen) - - # Look for tests in a class's contained objects. - if inspect.isclass(obj) and self._recurse: - for valname, val in obj.__dict__.items(): - # Check if this contained object should be ignored. - if self._filter(val, name, valname): - continue - # Special handling for staticmethod/classmethod. - if isinstance(val, staticmethod): - val = getattr(obj, valname) - if isinstance(val, classmethod): - val = getattr(obj, valname).im_func - - # Recurse to methods, properties, and nested classes. - if ((inspect.isfunction(val) or inspect.isclass(val) or - isinstance(val, property)) and - self._from_module(module, val)): - valname = '%s.%s' % (name, valname) - self._find(tests, val, valname, module, source_lines, - globs, seen) - - def _get_test(self, obj, name, module, globs, source_lines): - """ - Return a DocTest for the given object, if it defines a docstring; - otherwise, return None. - """ - # Extract the object's docstring. If it doesn't have one, - # then return None (no test for this object). - if isinstance(obj, basestring): - docstring = obj - else: - try: - if obj.__doc__ is None: - docstring = '' - else: - docstring = obj.__doc__ - if not isinstance(docstring, basestring): - docstring = str(docstring) - except (TypeError, AttributeError): - docstring = '' - - # Find the docstring's location in the file. - lineno = self._find_lineno(obj, source_lines) - - # Don't bother if the docstring is empty. - if self._exclude_empty and not docstring: - return None - - # Return a DocTest for this object. - if module is None: - filename = None - else: - filename = getattr(module, '__file__', module.__name__) - if filename[-4:] in (".pyc", ".pyo"): - filename = filename[:-1] - return self._parser.get_doctest(docstring, globs, name, - filename, lineno) - - def _find_lineno(self, obj, source_lines): - """ - Return a line number of the given object's docstring. Note: - this method assumes that the object has a docstring. - """ - lineno = None - - # Find the line number for modules. - if inspect.ismodule(obj): - lineno = 0 - - # Find the line number for classes. - # Note: this could be fooled if a class is defined multiple - # times in a single file. - if inspect.isclass(obj): - if source_lines is None: - return None - pat = re.compile(r'^\s*class\s*%s\b' % - getattr(obj, '__name__', '-')) - for i, line in enumerate(source_lines): - if pat.match(line): - lineno = i - break - - # Find the line number for functions & methods. - if inspect.ismethod(obj): obj = obj.im_func - if inspect.isfunction(obj): obj = obj.func_code - if inspect.istraceback(obj): obj = obj.tb_frame - if inspect.isframe(obj): obj = obj.f_code - if inspect.iscode(obj): - lineno = getattr(obj, 'co_firstlineno', None)-1 - - # Find the line number where the docstring starts. Assume - # that it's the first line that begins with a quote mark. - # Note: this could be fooled by a multiline function - # signature, where a continuation line begins with a quote - # mark. - if lineno is not None: - if source_lines is None: - return lineno+1 - pat = re.compile('(^|.*:)\s*\w*("|\')') - for lineno in range(lineno, len(source_lines)): - if pat.match(source_lines[lineno]): - return lineno - - # We couldn't find the line number. - return None - -###################################################################### -## 5. DocTest Runner -###################################################################### - -class DocTestRunner: - """ - A class used to run DocTest test cases, and accumulate statistics. - The `run` method is used to process a single DocTest case. It - returns a tuple `(f, t)`, where `t` is the number of test cases - tried, and `f` is the number of test cases that failed. - - >>> tests = DocTestFinder().find(_TestClass) - >>> runner = DocTestRunner(verbose=False) - >>> for test in tests: - ... print runner.run(test) - (0, 2) - (0, 1) - (0, 2) - (0, 2) - - The `summarize` method prints a summary of all the test cases that - have been run by the runner, and returns an aggregated `(f, t)` - tuple: - - >>> runner.summarize(verbose=1) - 4 items passed all tests: - 2 tests in _TestClass - 2 tests in _TestClass.__init__ - 2 tests in _TestClass.get - 1 tests in _TestClass.square - 7 tests in 4 items. - 7 passed and 0 failed. - Test passed. - (0, 7) - - The aggregated number of tried examples and failed examples is - also available via the `tries` and `failures` attributes: - - >>> runner.tries - 7 - >>> runner.failures - 0 - - The comparison between expected outputs and actual outputs is done - by an `OutputChecker`. This comparison may be customized with a - number of option flags; see the documentation for `testmod` for - more information. If the option flags are insufficient, then the - comparison may also be customized by passing a subclass of - `OutputChecker` to the constructor. - - The test runner's display output can be controlled in two ways. - First, an output function (`out) can be passed to - `TestRunner.run`; this function will be called with strings that - should be displayed. It defaults to `sys.stdout.write`. If - capturing the output is not sufficient, then the display output - can be also customized by subclassing DocTestRunner, and - overriding the methods `report_start`, `report_success`, - `report_unexpected_exception`, and `report_failure`. - """ - # This divider string is used to separate failure messages, and to - # separate sections of the summary. - DIVIDER = "*" * 70 - - def __init__(self, checker=None, verbose=None, optionflags=0): - """ - Create a new test runner. - - Optional keyword arg `checker` is the `OutputChecker` that - should be used to compare the expected outputs and actual - outputs of doctest examples. - - Optional keyword arg 'verbose' prints lots of stuff if true, - only failures if false; by default, it's true iff '-v' is in - sys.argv. - - Optional argument `optionflags` can be used to control how the - test runner compares expected output to actual output, and how - it displays failures. See the documentation for `testmod` for - more information. - """ - self._checker = checker or OutputChecker() - if verbose is None: - verbose = '-v' in sys.argv - self._verbose = verbose - self.optionflags = optionflags - self.original_optionflags = optionflags - - # Keep track of the examples we've run. - self.tries = 0 - self.failures = 0 - self._name2ft = {} - - # Create a fake output target for capturing doctest output. - self._fakeout = _SpoofOut() - - #///////////////////////////////////////////////////////////////// - # Reporting methods - #///////////////////////////////////////////////////////////////// - - def report_start(self, out, test, example): - """ - Report that the test runner is about to process the given - example. (Only displays a message if verbose=True) - """ - if self._verbose: - if example.want: - out('Trying:\n' + _indent(example.source) + - 'Expecting:\n' + _indent(example.want)) - else: - out('Trying:\n' + _indent(example.source) + - 'Expecting nothing\n') - - def report_success(self, out, test, example, got): - """ - Report that the given example ran successfully. (Only - displays a message if verbose=True) - """ - if self._verbose: - out("ok\n") - - def report_failure(self, out, test, example, got): - """ - Report that the given example failed. - """ - out(self._failure_header(test, example) + - self._checker.output_difference(example, got, self.optionflags)) - - def report_unexpected_exception(self, out, test, example, exc_info): - """ - Report that the given example raised an unexpected exception. - """ - out(self._failure_header(test, example) + - 'Exception raised:\n' + _indent(_exception_traceback(exc_info))) - - def _failure_header(self, test, example): - out = [self.DIVIDER] - if test.filename: - if test.lineno is not None and example.lineno is not None: - lineno = test.lineno + example.lineno + 1 - else: - lineno = '?' - out.append('File "%s", line %s, in %s' % - (test.filename, lineno, test.name)) - else: - out.append('Line %s, in %s' % (example.lineno+1, test.name)) - out.append('Failed example:') - source = example.source - out.append(_indent(source)) - return '\n'.join(out) - - #///////////////////////////////////////////////////////////////// - # DocTest Running - #///////////////////////////////////////////////////////////////// - - def __run(self, test, compileflags, out): - """ - Run the examples in `test`. Write the outcome of each example - with one of the `DocTestRunner.report_*` methods, using the - writer function `out`. `compileflags` is the set of compiler - flags that should be used to execute examples. Return a tuple - `(f, t)`, where `t` is the number of examples tried, and `f` - is the number of examples that failed. The examples are run - in the namespace `test.globs`. - """ - # Keep track of the number of failures and tries. - failures = tries = 0 - - # Save the option flags (since option directives can be used - # to modify them). - original_optionflags = self.optionflags - - SUCCESS, FAILURE, BOOM = range(3) # `outcome` state - - check = self._checker.check_output - - # Process each example. - for examplenum, example in enumerate(test.examples): - - # If REPORT_ONLY_FIRST_FAILURE is set, then suppress - # reporting after the first failure. - quiet = (self.optionflags & REPORT_ONLY_FIRST_FAILURE and - failures > 0) - - # Merge in the example's options. - self.optionflags = original_optionflags - if example.options: - for (optionflag, val) in example.options.items(): - if val: - self.optionflags |= optionflag - else: - self.optionflags &= ~optionflag - - # Record that we started this example. - tries += 1 - if not quiet: - self.report_start(out, test, example) - - # Use a special filename for compile(), so we can retrieve - # the source code during interactive debugging (see - # __patched_linecache_getlines). - filename = '' % (test.name, examplenum) - - # Run the example in the given context (globs), and record - # any exception that gets raised. (But don't intercept - # keyboard interrupts.) - try: - # Don't blink! This is where the user's code gets run. - exec compile(example.source, filename, "single", - compileflags, 1) in test.globs - self.debugger.set_continue() # ==== Example Finished ==== - exception = None - except KeyboardInterrupt: - raise - except: - exception = sys.exc_info() - self.debugger.set_continue() # ==== Example Finished ==== - - got = self._fakeout.getvalue() # the actual output - self._fakeout.truncate(0) - outcome = FAILURE # guilty until proved innocent or insane - - # If the example executed without raising any exceptions, - # verify its output. - if exception is None: - if check(example.want, got, self.optionflags): - outcome = SUCCESS - - # The example raised an exception: check if it was expected. - else: - exc_info = sys.exc_info() - exc_msg = traceback.format_exception_only(*exc_info[:2])[-1] - if not quiet: - got += _exception_traceback(exc_info) - - # If `example.exc_msg` is None, then we weren't expecting - # an exception. - if example.exc_msg is None: - outcome = BOOM - - # We expected an exception: see whether it matches. - elif check(example.exc_msg, exc_msg, self.optionflags): - outcome = SUCCESS - - # Another chance if they didn't care about the detail. - elif self.optionflags & IGNORE_EXCEPTION_DETAIL: - m1 = re.match(r'[^:]*:', example.exc_msg) - m2 = re.match(r'[^:]*:', exc_msg) - if m1 and m2 and check(m1.group(0), m2.group(0), - self.optionflags): - outcome = SUCCESS - - # Report the outcome. - if outcome is SUCCESS: - if not quiet: - self.report_success(out, test, example, got) - elif outcome is FAILURE: - if not quiet: - self.report_failure(out, test, example, got) - failures += 1 - elif outcome is BOOM: - if not quiet: - self.report_unexpected_exception(out, test, example, - exc_info) - failures += 1 - else: - assert False, ("unknown outcome", outcome) - - # Restore the option flags (in case they were modified) - self.optionflags = original_optionflags - - # Record and return the number of failures and tries. - self.__record_outcome(test, failures, tries) - return failures, tries - - def __record_outcome(self, test, f, t): - """ - Record the fact that the given DocTest (`test`) generated `f` - failures out of `t` tried examples. - """ - f2, t2 = self._name2ft.get(test.name, (0,0)) - self._name2ft[test.name] = (f+f2, t+t2) - self.failures += f - self.tries += t - - __LINECACHE_FILENAME_RE = re.compile(r'[\w\.]+)' - r'\[(?P\d+)\]>$') - def __patched_linecache_getlines(self, filename): - m = self.__LINECACHE_FILENAME_RE.match(filename) - if m and m.group('name') == self.test.name: - example = self.test.examples[int(m.group('examplenum'))] - return example.source.splitlines(True) - else: - return self.save_linecache_getlines(filename) - - def run(self, test, compileflags=None, out=None, clear_globs=True): - """ - Run the examples in `test`, and display the results using the - writer function `out`. - - The examples are run in the namespace `test.globs`. If - `clear_globs` is true (the default), then this namespace will - be cleared after the test runs, to help with garbage - collection. If you would like to examine the namespace after - the test completes, then use `clear_globs=False`. - - `compileflags` gives the set of flags that should be used by - the Python compiler when running the examples. If not - specified, then it will default to the set of future-import - flags that apply to `globs`. - - The output of each example is checked using - `DocTestRunner.check_output`, and the results are formatted by - the `DocTestRunner.report_*` methods. - """ - self.test = test - - if compileflags is None: - compileflags = _extract_future_flags(test.globs) - - save_stdout = sys.stdout - if out is None: - out = save_stdout.write - sys.stdout = self._fakeout - - # Patch pdb.set_trace to restore sys.stdout during interactive - # debugging (so it's not still redirected to self._fakeout). - # Note that the interactive output will go to *our* - # save_stdout, even if that's not the real sys.stdout; this - # allows us to write test cases for the set_trace behavior. - save_set_trace = pdb.set_trace - self.debugger = _OutputRedirectingPdb(save_stdout) - self.debugger.reset() - pdb.set_trace = self.debugger.set_trace - - # Patch linecache.getlines, so we can see the example's source - # when we're inside the debugger. - self.save_linecache_getlines = linecache.getlines - linecache.getlines = self.__patched_linecache_getlines - - try: - return self.__run(test, compileflags, out) - finally: - sys.stdout = save_stdout - pdb.set_trace = save_set_trace - linecache.getlines = self.save_linecache_getlines - if clear_globs: - test.globs.clear() - - #///////////////////////////////////////////////////////////////// - # Summarization - #///////////////////////////////////////////////////////////////// - def summarize(self, verbose=None): - """ - Print a summary of all the test cases that have been run by - this DocTestRunner, and return a tuple `(f, t)`, where `f` is - the total number of failed examples, and `t` is the total - number of tried examples. - - The optional `verbose` argument controls how detailed the - summary is. If the verbosity is not specified, then the - DocTestRunner's verbosity is used. - """ - if verbose is None: - verbose = self._verbose - notests = [] - passed = [] - failed = [] - totalt = totalf = 0 - for x in self._name2ft.items(): - name, (f, t) = x - assert f <= t - totalt += t - totalf += f - if t == 0: - notests.append(name) - elif f == 0: - passed.append( (name, t) ) - else: - failed.append(x) - if verbose: - if notests: - print len(notests), "items had no tests:" - notests.sort() - for thing in notests: - print " ", thing - if passed: - print len(passed), "items passed all tests:" - passed.sort() - for thing, count in passed: - print " %3d tests in %s" % (count, thing) - if failed: - print self.DIVIDER - print len(failed), "items had failures:" - failed.sort() - for thing, (f, t) in failed: - print " %3d of %3d in %s" % (f, t, thing) - if verbose: - print totalt, "tests in", len(self._name2ft), "items." - print totalt - totalf, "passed and", totalf, "failed." - if totalf: - print "***Test Failed***", totalf, "failures." - elif verbose: - print "Test passed." - return totalf, totalt - - #///////////////////////////////////////////////////////////////// - # Backward compatibility cruft to maintain doctest.master. - #///////////////////////////////////////////////////////////////// - def merge(self, other): - d = self._name2ft - for name, (f, t) in other._name2ft.items(): - if name in d: - print "*** DocTestRunner.merge: '" + name + "' in both" \ - " testers; summing outcomes." - f2, t2 = d[name] - f = f + f2 - t = t + t2 - d[name] = f, t - -class OutputChecker: - """ - A class used to check the whether the actual output from a doctest - example matches the expected output. `OutputChecker` defines two - methods: `check_output`, which compares a given pair of outputs, - and returns true if they match; and `output_difference`, which - returns a string describing the differences between two outputs. - """ - def check_output(self, want, got, optionflags): - """ - Return True iff the actual output from an example (`got`) - matches the expected output (`want`). These strings are - always considered to match if they are identical; but - depending on what option flags the test runner is using, - several non-exact match types are also possible. See the - documentation for `TestRunner` for more information about - option flags. - """ - # Handle the common case first, for efficiency: - # if they're string-identical, always return true. - if got == want: - return True - - # The values True and False replaced 1 and 0 as the return - # value for boolean comparisons in Python 2.3. - if not (optionflags & DONT_ACCEPT_TRUE_FOR_1): - if (got,want) == ("True\n", "1\n"): - return True - if (got,want) == ("False\n", "0\n"): - return True - - # can be used as a special sequence to signify a - # blank line, unless the DONT_ACCEPT_BLANKLINE flag is used. - if not (optionflags & DONT_ACCEPT_BLANKLINE): - # Replace in want with a blank line. - want = re.sub('(?m)^%s\s*?$' % re.escape(BLANKLINE_MARKER), - '', want) - # If a line in got contains only spaces, then remove the - # spaces. - got = re.sub('(?m)^\s*?$', '', got) - if got == want: - return True - - # This flag causes doctest to ignore any differences in the - # contents of whitespace strings. Note that this can be used - # in conjunction with the ELLIPSIS flag. - if optionflags & NORMALIZE_WHITESPACE: - got = ' '.join(got.split()) - want = ' '.join(want.split()) - if got == want: - return True - - # The ELLIPSIS flag says to let the sequence "..." in `want` - # match any substring in `got`. - if optionflags & ELLIPSIS: - if _ellipsis_match(want, got): - return True - - # We didn't find any match; return false. - return False - - # Should we do a fancy diff? - def _do_a_fancy_diff(self, want, got, optionflags): - # Not unless they asked for a fancy diff. - if not optionflags & (REPORT_UDIFF | - REPORT_CDIFF | - REPORT_NDIFF): - return False - - # If expected output uses ellipsis, a meaningful fancy diff is - # too hard ... or maybe not. In two real-life failures Tim saw, - # a diff was a major help anyway, so this is commented out. - # [todo] _ellipsis_match() knows which pieces do and don't match, - # and could be the basis for a kick-ass diff in this case. - ##if optionflags & ELLIPSIS and ELLIPSIS_MARKER in want: - ## return False - - # ndiff does intraline difference marking, so can be useful even - # for 1-line differences. - if optionflags & REPORT_NDIFF: - return True - - # The other diff types need at least a few lines to be helpful. - return want.count('\n') > 2 and got.count('\n') > 2 - - def output_difference(self, example, got, optionflags): - """ - Return a string describing the differences between the - expected output for a given example (`example`) and the actual - output (`got`). `optionflags` is the set of option flags used - to compare `want` and `got`. - """ - want = example.want - # If s are being used, then replace blank lines - # with in the actual output string. - if not (optionflags & DONT_ACCEPT_BLANKLINE): - got = re.sub('(?m)^[ ]*(?=\n)', BLANKLINE_MARKER, got) - - # Check if we should use diff. - if self._do_a_fancy_diff(want, got, optionflags): - # Split want & got into lines. - want_lines = want.splitlines(True) # True == keep line ends - got_lines = got.splitlines(True) - # Use difflib to find their differences. - if optionflags & REPORT_UDIFF: - diff = difflib.unified_diff(want_lines, got_lines, n=2) - diff = list(diff)[2:] # strip the diff header - kind = 'unified diff with -expected +actual' - elif optionflags & REPORT_CDIFF: - diff = difflib.context_diff(want_lines, got_lines, n=2) - diff = list(diff)[2:] # strip the diff header - kind = 'context diff with expected followed by actual' - elif optionflags & REPORT_NDIFF: - engine = difflib.Differ(charjunk=difflib.IS_CHARACTER_JUNK) - diff = list(engine.compare(want_lines, got_lines)) - kind = 'ndiff with -expected +actual' - else: - assert 0, 'Bad diff option' - # Remove trailing whitespace on diff output. - diff = [line.rstrip() + '\n' for line in diff] - return 'Differences (%s):\n' % kind + _indent(''.join(diff)) - - # If we're not using diff, then simply list the expected - # output followed by the actual output. - if want and got: - return 'Expected:\n%sGot:\n%s' % (_indent(want), _indent(got)) - elif want: - return 'Expected:\n%sGot nothing\n' % _indent(want) - elif got: - return 'Expected nothing\nGot:\n%s' % _indent(got) - else: - return 'Expected nothing\nGot nothing\n' - -class DocTestFailure(Exception): - """A DocTest example has failed in debugging mode. - - The exception instance has variables: - - - test: the DocTest object being run - - - excample: the Example object that failed - - - got: the actual output - """ - def __init__(self, test, example, got): - self.test = test - self.example = example - self.got = got - - def __str__(self): - return str(self.test) - -class UnexpectedException(Exception): - """A DocTest example has encountered an unexpected exception - - The exception instance has variables: - - - test: the DocTest object being run - - - excample: the Example object that failed - - - exc_info: the exception info - """ - def __init__(self, test, example, exc_info): - self.test = test - self.example = example - self.exc_info = exc_info - - def __str__(self): - return str(self.test) - -class DebugRunner(DocTestRunner): - r"""Run doc tests but raise an exception as soon as there is a failure. - - If an unexpected exception occurs, an UnexpectedException is raised. - It contains the test, the example, and the original exception: - - >>> runner = DebugRunner(verbose=False) - >>> test = DocTestParser().get_doctest('>>> raise KeyError\n42', - ... {}, 'foo', 'foo.py', 0) - >>> try: - ... runner.run(test) - ... except UnexpectedException, failure: - ... pass - - >>> failure.test is test - True - - >>> failure.example.want - '42\n' - - >>> exc_info = failure.exc_info - >>> raise exc_info[0], exc_info[1], exc_info[2] - Traceback (most recent call last): - ... - KeyError - - We wrap the original exception to give the calling application - access to the test and example information. - - If the output doesn't match, then a DocTestFailure is raised: - - >>> test = DocTestParser().get_doctest(''' - ... >>> x = 1 - ... >>> x - ... 2 - ... ''', {}, 'foo', 'foo.py', 0) - - >>> try: - ... runner.run(test) - ... except DocTestFailure, failure: - ... pass - - DocTestFailure objects provide access to the test: - - >>> failure.test is test - True - - As well as to the example: - - >>> failure.example.want - '2\n' - - and the actual output: - - >>> failure.got - '1\n' - - If a failure or error occurs, the globals are left intact: - - >>> del test.globs['__builtins__'] - >>> test.globs - {'x': 1} - - >>> test = DocTestParser().get_doctest(''' - ... >>> x = 2 - ... >>> raise KeyError - ... ''', {}, 'foo', 'foo.py', 0) - - >>> runner.run(test) - Traceback (most recent call last): - ... - UnexpectedException: - - >>> del test.globs['__builtins__'] - >>> test.globs - {'x': 2} - - But the globals are cleared if there is no error: - - >>> test = DocTestParser().get_doctest(''' - ... >>> x = 2 - ... ''', {}, 'foo', 'foo.py', 0) - - >>> runner.run(test) - (0, 1) - - >>> test.globs - {} - - """ - - def run(self, test, compileflags=None, out=None, clear_globs=True): - r = DocTestRunner.run(self, test, compileflags, out, False) - if clear_globs: - test.globs.clear() - return r - - def report_unexpected_exception(self, out, test, example, exc_info): - raise UnexpectedException(test, example, exc_info) - - def report_failure(self, out, test, example, got): - raise DocTestFailure(test, example, got) - -###################################################################### -## 6. Test Functions -###################################################################### -# These should be backwards compatible. - -# For backward compatibility, a global instance of a DocTestRunner -# class, updated by testmod. -master = None - -def testmod(m=None, name=None, globs=None, verbose=None, isprivate=None, - report=True, optionflags=0, extraglobs=None, - raise_on_error=False, exclude_empty=False): - """m=None, name=None, globs=None, verbose=None, isprivate=None, - report=True, optionflags=0, extraglobs=None, raise_on_error=False, - exclude_empty=False - - Test examples in docstrings in functions and classes reachable - from module m (or the current module if m is not supplied), starting - with m.__doc__. Unless isprivate is specified, private names - are not skipped. - - Also test examples reachable from dict m.__test__ if it exists and is - not None. m.__test__ maps names to functions, classes and strings; - function and class docstrings are tested even if the name is private; - strings are tested directly, as if they were docstrings. - - Return (#failures, #tests). - - See doctest.__doc__ for an overview. - - Optional keyword arg "name" gives the name of the module; by default - use m.__name__. - - Optional keyword arg "globs" gives a dict to be used as the globals - when executing examples; by default, use m.__dict__. A copy of this - dict is actually used for each docstring, so that each docstring's - examples start with a clean slate. - - Optional keyword arg "extraglobs" gives a dictionary that should be - merged into the globals that are used to execute examples. By - default, no extra globals are used. This is new in 2.4. - - Optional keyword arg "verbose" prints lots of stuff if true, prints - only failures if false; by default, it's true iff "-v" is in sys.argv. - - Optional keyword arg "report" prints a summary at the end when true, - else prints nothing at the end. In verbose mode, the summary is - detailed, else very brief (in fact, empty if all tests passed). - - Optional keyword arg "optionflags" or's together module constants, - and defaults to 0. This is new in 2.3. Possible values (see the - docs for details): - - DONT_ACCEPT_TRUE_FOR_1 - DONT_ACCEPT_BLANKLINE - NORMALIZE_WHITESPACE - ELLIPSIS - IGNORE_EXCEPTION_DETAIL - REPORT_UDIFF - REPORT_CDIFF - REPORT_NDIFF - REPORT_ONLY_FIRST_FAILURE - - Optional keyword arg "raise_on_error" raises an exception on the - first unexpected exception or failure. This allows failures to be - post-mortem debugged. - - Deprecated in Python 2.4: - Optional keyword arg "isprivate" specifies a function used to - determine whether a name is private. The default function is - treat all functions as public. Optionally, "isprivate" can be - set to doctest.is_private to skip over functions marked as private - using the underscore naming convention; see its docs for details. - - Advanced tomfoolery: testmod runs methods of a local instance of - class doctest.Tester, then merges the results into (or creates) - global Tester instance doctest.master. Methods of doctest.master - can be called directly too, if you want to do something unusual. - Passing report=0 to testmod is especially useful then, to delay - displaying a summary. Invoke doctest.master.summarize(verbose) - when you're done fiddling. - """ - global master - - if isprivate is not None: - warnings.warn("the isprivate argument is deprecated; " - "examine DocTestFinder.find() lists instead", - DeprecationWarning) - - # If no module was given, then use __main__. - if m is None: - # DWA - m will still be None if this wasn't invoked from the command - # line, in which case the following TypeError is about as good an error - # as we should expect - m = sys.modules.get('__main__') - - # Check that we were actually given a module. - if not inspect.ismodule(m): - raise TypeError("testmod: module required; %r" % (m,)) - - # If no name was given, then use the module's name. - if name is None: - name = m.__name__ - - # Find, parse, and run all tests in the given module. - finder = DocTestFinder(_namefilter=isprivate, exclude_empty=exclude_empty) - - if raise_on_error: - runner = DebugRunner(verbose=verbose, optionflags=optionflags) - else: - runner = DocTestRunner(verbose=verbose, optionflags=optionflags) - - for test in finder.find(m, name, globs=globs, extraglobs=extraglobs): - runner.run(test) - - if report: - runner.summarize() - - if master is None: - master = runner - else: - master.merge(runner) - - return runner.failures, runner.tries - -def testfile(filename, module_relative=True, name=None, package=None, - globs=None, verbose=None, report=True, optionflags=0, - extraglobs=None, raise_on_error=False, parser=DocTestParser()): - """ - Test examples in the given file. Return (#failures, #tests). - - Optional keyword arg "module_relative" specifies how filenames - should be interpreted: - - - If "module_relative" is True (the default), then "filename" - specifies a module-relative path. By default, this path is - relative to the calling module's directory; but if the - "package" argument is specified, then it is relative to that - package. To ensure os-independence, "filename" should use - "/" characters to separate path segments, and should not - be an absolute path (i.e., it may not begin with "/"). - - - If "module_relative" is False, then "filename" specifies an - os-specific path. The path may be absolute or relative (to - the current working directory). - - Optional keyword arg "name" gives the name of the test; by default - use the file's basename. - - Optional keyword argument "package" is a Python package or the - name of a Python package whose directory should be used as the - base directory for a module relative filename. If no package is - specified, then the calling module's directory is used as the base - directory for module relative filenames. It is an error to - specify "package" if "module_relative" is False. - - Optional keyword arg "globs" gives a dict to be used as the globals - when executing examples; by default, use {}. A copy of this dict - is actually used for each docstring, so that each docstring's - examples start with a clean slate. - - Optional keyword arg "extraglobs" gives a dictionary that should be - merged into the globals that are used to execute examples. By - default, no extra globals are used. - - Optional keyword arg "verbose" prints lots of stuff if true, prints - only failures if false; by default, it's true iff "-v" is in sys.argv. - - Optional keyword arg "report" prints a summary at the end when true, - else prints nothing at the end. In verbose mode, the summary is - detailed, else very brief (in fact, empty if all tests passed). - - Optional keyword arg "optionflags" or's together module constants, - and defaults to 0. Possible values (see the docs for details): - - DONT_ACCEPT_TRUE_FOR_1 - DONT_ACCEPT_BLANKLINE - NORMALIZE_WHITESPACE - ELLIPSIS - IGNORE_EXCEPTION_DETAIL - REPORT_UDIFF - REPORT_CDIFF - REPORT_NDIFF - REPORT_ONLY_FIRST_FAILURE - - Optional keyword arg "raise_on_error" raises an exception on the - first unexpected exception or failure. This allows failures to be - post-mortem debugged. - - Optional keyword arg "parser" specifies a DocTestParser (or - subclass) that should be used to extract tests from the files. - - Advanced tomfoolery: testmod runs methods of a local instance of - class doctest.Tester, then merges the results into (or creates) - global Tester instance doctest.master. Methods of doctest.master - can be called directly too, if you want to do something unusual. - Passing report=0 to testmod is especially useful then, to delay - displaying a summary. Invoke doctest.master.summarize(verbose) - when you're done fiddling. - """ - global master - - if package and not module_relative: - raise ValueError("Package may only be specified for module-" - "relative paths.") - - # Relativize the path - if module_relative: - package = _normalize_module(package) - filename = _module_relative_path(package, filename) - - # If no name was given, then use the file's name. - if name is None: - name = os.path.basename(filename) - - # Assemble the globals. - if globs is None: - globs = {} - else: - globs = globs.copy() - if extraglobs is not None: - globs.update(extraglobs) - - if raise_on_error: - runner = DebugRunner(verbose=verbose, optionflags=optionflags) - else: - runner = DocTestRunner(verbose=verbose, optionflags=optionflags) - - # Read the file, convert it to a test, and run it. - s = open(filename).read() - test = parser.get_doctest(s, globs, name, filename, 0) - runner.run(test) - - if report: - runner.summarize() - - if master is None: - master = runner - else: - master.merge(runner) - - return runner.failures, runner.tries - -def run_docstring_examples(f, globs, verbose=False, name="NoName", - compileflags=None, optionflags=0): - """ - Test examples in the given object's docstring (`f`), using `globs` - as globals. Optional argument `name` is used in failure messages. - If the optional argument `verbose` is true, then generate output - even if there are no failures. - - `compileflags` gives the set of flags that should be used by the - Python compiler when running the examples. If not specified, then - it will default to the set of future-import flags that apply to - `globs`. - - Optional keyword arg `optionflags` specifies options for the - testing and output. See the documentation for `testmod` for more - information. - """ - # Find, parse, and run all tests in the given module. - finder = DocTestFinder(verbose=verbose, recurse=False) - runner = DocTestRunner(verbose=verbose, optionflags=optionflags) - for test in finder.find(f, name, globs=globs): - runner.run(test, compileflags=compileflags) - -###################################################################### -## 7. Tester -###################################################################### -# This is provided only for backwards compatibility. It's not -# actually used in any way. - -class Tester: - def __init__(self, mod=None, globs=None, verbose=None, - isprivate=None, optionflags=0): - - warnings.warn("class Tester is deprecated; " - "use class doctest.DocTestRunner instead", - DeprecationWarning, stacklevel=2) - if mod is None and globs is None: - raise TypeError("Tester.__init__: must specify mod or globs") - if mod is not None and not inspect.ismodule(mod): - raise TypeError("Tester.__init__: mod must be a module; %r" % - (mod,)) - if globs is None: - globs = mod.__dict__ - self.globs = globs - - self.verbose = verbose - self.isprivate = isprivate - self.optionflags = optionflags - self.testfinder = DocTestFinder(_namefilter=isprivate) - self.testrunner = DocTestRunner(verbose=verbose, - optionflags=optionflags) - - def runstring(self, s, name): - test = DocTestParser().get_doctest(s, self.globs, name, None, None) - if self.verbose: - print "Running string", name - (f,t) = self.testrunner.run(test) - if self.verbose: - print f, "of", t, "examples failed in string", name - return (f,t) - - def rundoc(self, object, name=None, module=None): - f = t = 0 - tests = self.testfinder.find(object, name, module=module, - globs=self.globs) - for test in tests: - (f2, t2) = self.testrunner.run(test) - (f,t) = (f+f2, t+t2) - return (f,t) - - def rundict(self, d, name, module=None): - import new - m = new.module(name) - m.__dict__.update(d) - if module is None: - module = False - return self.rundoc(m, name, module) - - def run__test__(self, d, name): - import new - m = new.module(name) - m.__test__ = d - return self.rundoc(m, name) - - def summarize(self, verbose=None): - return self.testrunner.summarize(verbose) - - def merge(self, other): - self.testrunner.merge(other.testrunner) - -###################################################################### -## 8. Unittest Support -###################################################################### - -_unittest_reportflags = 0 - -def set_unittest_reportflags(flags): - """Sets the unittest option flags. - - The old flag is returned so that a runner could restore the old - value if it wished to: - - >>> old = _unittest_reportflags - >>> set_unittest_reportflags(REPORT_NDIFF | - ... REPORT_ONLY_FIRST_FAILURE) == old - True - - >>> import doctest - >>> doctest._unittest_reportflags == (REPORT_NDIFF | - ... REPORT_ONLY_FIRST_FAILURE) - True - - Only reporting flags can be set: - - >>> set_unittest_reportflags(ELLIPSIS) - Traceback (most recent call last): - ... - ValueError: ('Only reporting flags allowed', 8) - - >>> set_unittest_reportflags(old) == (REPORT_NDIFF | - ... REPORT_ONLY_FIRST_FAILURE) - True - """ - global _unittest_reportflags - - if (flags & REPORTING_FLAGS) != flags: - raise ValueError("Only reporting flags allowed", flags) - old = _unittest_reportflags - _unittest_reportflags = flags - return old - - -class DocTestCase(unittest.TestCase): - - def __init__(self, test, optionflags=0, setUp=None, tearDown=None, - checker=None): - - unittest.TestCase.__init__(self) - self._dt_optionflags = optionflags - self._dt_checker = checker - self._dt_test = test - self._dt_setUp = setUp - self._dt_tearDown = tearDown - - def setUp(self): - test = self._dt_test - - if self._dt_setUp is not None: - self._dt_setUp(test) - - def tearDown(self): - test = self._dt_test - - if self._dt_tearDown is not None: - self._dt_tearDown(test) - - test.globs.clear() - - def runTest(self): - test = self._dt_test - old = sys.stdout - new = StringIO() - optionflags = self._dt_optionflags - - if not (optionflags & REPORTING_FLAGS): - # The option flags don't include any reporting flags, - # so add the default reporting flags - optionflags |= _unittest_reportflags - - runner = DocTestRunner(optionflags=optionflags, - checker=self._dt_checker, verbose=False) - - try: - runner.DIVIDER = "-"*70 - failures, tries = runner.run( - test, out=new.write, clear_globs=False) - finally: - sys.stdout = old - - if failures: - raise self.failureException(self.format_failure(new.getvalue())) - - def format_failure(self, err): - test = self._dt_test - if test.lineno is None: - lineno = 'unknown line number' - else: - lineno = '%s' % test.lineno - lname = '.'.join(test.name.split('.')[-1:]) - return ('Failed doctest test for %s\n' - ' File "%s", line %s, in %s\n\n%s' - % (test.name, test.filename, lineno, lname, err) - ) - - def debug(self): - r"""Run the test case without results and without catching exceptions - - The unit test framework includes a debug method on test cases - and test suites to support post-mortem debugging. The test code - is run in such a way that errors are not caught. This way a - caller can catch the errors and initiate post-mortem debugging. - - The DocTestCase provides a debug method that raises - UnexpectedException errors if there is an unexepcted - exception: - - >>> test = DocTestParser().get_doctest('>>> raise KeyError\n42', - ... {}, 'foo', 'foo.py', 0) - >>> case = DocTestCase(test) - >>> try: - ... case.debug() - ... except UnexpectedException, failure: - ... pass - - The UnexpectedException contains the test, the example, and - the original exception: - - >>> failure.test is test - True - - >>> failure.example.want - '42\n' - - >>> exc_info = failure.exc_info - >>> raise exc_info[0], exc_info[1], exc_info[2] - Traceback (most recent call last): - ... - KeyError - - If the output doesn't match, then a DocTestFailure is raised: - - >>> test = DocTestParser().get_doctest(''' - ... >>> x = 1 - ... >>> x - ... 2 - ... ''', {}, 'foo', 'foo.py', 0) - >>> case = DocTestCase(test) - - >>> try: - ... case.debug() - ... except DocTestFailure, failure: - ... pass - - DocTestFailure objects provide access to the test: - - >>> failure.test is test - True - - As well as to the example: - - >>> failure.example.want - '2\n' - - and the actual output: - - >>> failure.got - '1\n' - - """ - - self.setUp() - runner = DebugRunner(optionflags=self._dt_optionflags, - checker=self._dt_checker, verbose=False) - runner.run(self._dt_test) - self.tearDown() - - def id(self): - return self._dt_test.name - - def __repr__(self): - name = self._dt_test.name.split('.') - return "%s (%s)" % (name[-1], '.'.join(name[:-1])) - - __str__ = __repr__ - - def shortDescription(self): - return "Doctest: " + self._dt_test.name - -def DocTestSuite(module=None, globs=None, extraglobs=None, test_finder=None, - **options): - """ - Convert doctest tests for a module to a unittest test suite. - - This converts each documentation string in a module that - contains doctest tests to a unittest test case. If any of the - tests in a doc string fail, then the test case fails. An exception - is raised showing the name of the file containing the test and a - (sometimes approximate) line number. - - The `module` argument provides the module to be tested. The argument - can be either a module or a module name. - - If no argument is given, the calling module is used. - - A number of options may be provided as keyword arguments: - - setUp - A set-up function. This is called before running the - tests in each file. The setUp function will be passed a DocTest - object. The setUp function can access the test globals as the - globs attribute of the test passed. - - tearDown - A tear-down function. This is called after running the - tests in each file. The tearDown function will be passed a DocTest - object. The tearDown function can access the test globals as the - globs attribute of the test passed. - - globs - A dictionary containing initial global variables for the tests. - - optionflags - A set of doctest option flags expressed as an integer. - """ - - if test_finder is None: - test_finder = DocTestFinder() - - module = _normalize_module(module) - tests = test_finder.find(module, globs=globs, extraglobs=extraglobs) - if globs is None: - globs = module.__dict__ - if not tests: - # Why do we want to do this? Because it reveals a bug that might - # otherwise be hidden. - raise ValueError(module, "has no tests") - - tests.sort() - suite = unittest.TestSuite() - for test in tests: - if len(test.examples) == 0: - continue - if not test.filename: - filename = module.__file__ - if filename[-4:] in (".pyc", ".pyo"): - filename = filename[:-1] - test.filename = filename - suite.addTest(DocTestCase(test, **options)) - - return suite - -class DocFileCase(DocTestCase): - - def id(self): - return '_'.join(self._dt_test.name.split('.')) - - def __repr__(self): - return self._dt_test.filename - __str__ = __repr__ - - def format_failure(self, err): - return ('Failed doctest test for %s\n File "%s", line 0\n\n%s' - % (self._dt_test.name, self._dt_test.filename, err) - ) - -def DocFileTest(path, module_relative=True, package=None, - globs=None, parser=DocTestParser(), **options): - if globs is None: - globs = {} - - if package and not module_relative: - raise ValueError("Package may only be specified for module-" - "relative paths.") - - # Relativize the path. - if module_relative: - package = _normalize_module(package) - path = _module_relative_path(package, path) - - # Find the file and read it. - name = os.path.basename(path) - doc = open(path).read() - - # Convert it to a test, and wrap it in a DocFileCase. - test = parser.get_doctest(doc, globs, name, path, 0) - return DocFileCase(test, **options) - -def DocFileSuite(*paths, **kw): - """A unittest suite for one or more doctest files. - - The path to each doctest file is given as a string; the - interpretation of that string depends on the keyword argument - "module_relative". - - A number of options may be provided as keyword arguments: - - module_relative - If "module_relative" is True, then the given file paths are - interpreted as os-independent module-relative paths. By - default, these paths are relative to the calling module's - directory; but if the "package" argument is specified, then - they are relative to that package. To ensure os-independence, - "filename" should use "/" characters to separate path - segments, and may not be an absolute path (i.e., it may not - begin with "/"). - - If "module_relative" is False, then the given file paths are - interpreted as os-specific paths. These paths may be absolute - or relative (to the current working directory). - - package - A Python package or the name of a Python package whose directory - should be used as the base directory for module relative paths. - If "package" is not specified, then the calling module's - directory is used as the base directory for module relative - filenames. It is an error to specify "package" if - "module_relative" is False. - - setUp - A set-up function. This is called before running the - tests in each file. The setUp function will be passed a DocTest - object. The setUp function can access the test globals as the - globs attribute of the test passed. - - tearDown - A tear-down function. This is called after running the - tests in each file. The tearDown function will be passed a DocTest - object. The tearDown function can access the test globals as the - globs attribute of the test passed. - - globs - A dictionary containing initial global variables for the tests. - - optionflags - A set of doctest option flags expressed as an integer. - - parser - A DocTestParser (or subclass) that should be used to extract - tests from the files. - """ - suite = unittest.TestSuite() - - # We do this here so that _normalize_module is called at the right - # level. If it were called in DocFileTest, then this function - # would be the caller and we might guess the package incorrectly. - if kw.get('module_relative', True): - kw['package'] = _normalize_module(kw.get('package')) - - for path in paths: - suite.addTest(DocFileTest(path, **kw)) - - return suite - -###################################################################### -## 9. Debugging Support -###################################################################### - -def script_from_examples(s): - r"""Extract script from text with examples. - - Converts text with examples to a Python script. Example input is - converted to regular code. Example output and all other words - are converted to comments: - - >>> text = ''' - ... Here are examples of simple math. - ... - ... Python has super accurate integer addition - ... - ... >>> 2 + 2 - ... 5 - ... - ... And very friendly error messages: - ... - ... >>> 1/0 - ... To Infinity - ... And - ... Beyond - ... - ... You can use logic if you want: - ... - ... >>> if 0: - ... ... blah - ... ... blah - ... ... - ... - ... Ho hum - ... ''' - - >>> print script_from_examples(text) - # Here are examples of simple math. - # - # Python has super accurate integer addition - # - 2 + 2 - # Expected: - ## 5 - # - # And very friendly error messages: - # - 1/0 - # Expected: - ## To Infinity - ## And - ## Beyond - # - # You can use logic if you want: - # - if 0: - blah - blah - # - # Ho hum - """ - output = [] - for piece in DocTestParser().parse(s): - if isinstance(piece, Example): - # Add the example's source code (strip trailing NL) - output.append(piece.source[:-1]) - # Add the expected output: - want = piece.want - if want: - output.append('# Expected:') - output += ['## '+l for l in want.split('\n')[:-1]] - else: - # Add non-example text. - output += [_comment_line(l) - for l in piece.split('\n')[:-1]] - - # Trim junk on both ends. - while output and output[-1] == '#': - output.pop() - while output and output[0] == '#': - output.pop(0) - # Combine the output, and return it. - return '\n'.join(output) - -def testsource(module, name): - """Extract the test sources from a doctest docstring as a script. - - Provide the module (or dotted name of the module) containing the - test to be debugged and the name (within the module) of the object - with the doc string with tests to be debugged. - """ - module = _normalize_module(module) - tests = DocTestFinder().find(module) - test = [t for t in tests if t.name == name] - if not test: - raise ValueError(name, "not found in tests") - test = test[0] - testsrc = script_from_examples(test.docstring) - return testsrc - -def debug_src(src, pm=False, globs=None): - """Debug a single doctest docstring, in argument `src`'""" - testsrc = script_from_examples(src) - debug_script(testsrc, pm, globs) - -def debug_script(src, pm=False, globs=None): - "Debug a test script. `src` is the script, as a string." - import pdb - - # Note that tempfile.NameTemporaryFile() cannot be used. As the - # docs say, a file so created cannot be opened by name a second time - # on modern Windows boxes, and execfile() needs to open it. - srcfilename = tempfile.mktemp(".py", "doctestdebug") - f = open(srcfilename, 'w') - f.write(src) - f.close() - - try: - if globs: - globs = globs.copy() - else: - globs = {} - - if pm: - try: - execfile(srcfilename, globs, globs) - except: - print sys.exc_info()[1] - pdb.post_mortem(sys.exc_info()[2]) - else: - # Note that %r is vital here. '%s' instead can, e.g., cause - # backslashes to get treated as metacharacters on Windows. - pdb.run("execfile(%r)" % srcfilename, globs, globs) - - finally: - os.remove(srcfilename) - -def debug(module, name, pm=False): - """Debug a single doctest docstring. - - Provide the module (or dotted name of the module) containing the - test to be debugged and the name (within the module) of the object - with the docstring with tests to be debugged. - """ - module = _normalize_module(module) - testsrc = testsource(module, name) - debug_script(testsrc, pm, module.__dict__) - -###################################################################### -## 10. Example Usage -###################################################################### -class _TestClass: - """ - A pointless class, for sanity-checking of docstring testing. - - Methods: - square() - get() - - >>> _TestClass(13).get() + _TestClass(-12).get() - 1 - >>> hex(_TestClass(13).square().get()) - '0xa9' - """ - - def __init__(self, val): - """val -> _TestClass object with associated value val. - - >>> t = _TestClass(123) - >>> print t.get() - 123 - """ - - self.val = val - - def square(self): - """square() -> square TestClass's associated value - - >>> _TestClass(13).square().get() - 169 - """ - - self.val = self.val ** 2 - return self - - def get(self): - """get() -> return TestClass's associated value. - - >>> x = _TestClass(-42) - >>> print x.get() - -42 - """ - - return self.val - -__test__ = {"_TestClass": _TestClass, - "string": r""" - Example of a string object, searched as-is. - >>> x = 1; y = 2 - >>> x + y, x * y - (3, 2) - """, - - "bool-int equivalence": r""" - In 2.2, boolean expressions displayed - 0 or 1. By default, we still accept - them. This can be disabled by passing - DONT_ACCEPT_TRUE_FOR_1 to the new - optionflags argument. - >>> 4 == 4 - 1 - >>> 4 == 4 - True - >>> 4 > 4 - 0 - >>> 4 > 4 - False - """, - - "blank lines": r""" - Blank lines can be marked with : - >>> print 'foo\n\nbar\n' - foo - - bar - - """, - - "ellipsis": r""" - If the ellipsis flag is used, then '...' can be used to - elide substrings in the desired output: - >>> print range(1000) #doctest: +ELLIPSIS - [0, 1, 2, ..., 999] - """, - - "whitespace normalization": r""" - If the whitespace normalization flag is used, then - differences in whitespace are ignored. - >>> print range(30) #doctest: +NORMALIZE_WHITESPACE - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, - 27, 28, 29] - """, - } - -def _test(): - r = unittest.TextTestRunner() - r.run(DocTestSuite()) - -if __name__ == "__main__": - _test() diff --git a/tests/modeltests/basic/models.py b/tests/modeltests/basic/models.py index 78d943eb97..acbea0d1e0 100644 --- a/tests/modeltests/basic/models.py +++ b/tests/modeltests/basic/models.py @@ -13,8 +13,7 @@ class Article(models.Model): def __str__(self): return self.headline -API_TESTS = """ - +__test__ = {'API_TESTS': """ # No articles are in the system yet. >>> Article.objects.all() [] @@ -314,14 +313,14 @@ AttributeError: Manager isn't accessible via Article instances >>> Article.objects.all() [, , , ] -""" +"""} from django.conf import settings building_docs = getattr(settings, 'BUILDING_DOCS', False) if building_docs or settings.DATABASE_ENGINE == 'postgresql': - API_TESTS += """ + __test__['API_TESTS'] += """ # In PostgreSQL, microsecond-level precision is available. >>> a9 = Article(headline='Article 9', pub_date=datetime(2005, 7, 31, 12, 30, 45, 180)) >>> a9.save() @@ -330,7 +329,7 @@ datetime.datetime(2005, 7, 31, 12, 30, 45, 180) """ if building_docs or settings.DATABASE_ENGINE == 'mysql': - API_TESTS += """ + __test__['API_TESTS'] += """ # In MySQL, microsecond-level precision isn't available. You'll lose # microsecond-level precision once the data is saved. >>> a9 = Article(headline='Article 9', pub_date=datetime(2005, 7, 31, 12, 30, 45, 180)) @@ -339,7 +338,7 @@ if building_docs or settings.DATABASE_ENGINE == 'mysql': datetime.datetime(2005, 7, 31, 12, 30, 45) """ -API_TESTS += """ +__test__['API_TESTS'] += """ # You can manually specify the primary key when creating a new object. >>> a101 = Article(id=101, headline='Article 101', pub_date=datetime(2005, 7, 31, 12, 30, 45)) diff --git a/tests/modeltests/choices/models.py b/tests/modeltests/choices/models.py index 881fb29fd2..37d36fe1d8 100644 --- a/tests/modeltests/choices/models.py +++ b/tests/modeltests/choices/models.py @@ -23,7 +23,7 @@ class Person(models.Model): def __str__(self): return self.name -API_TESTS = """ +__test__ = {'API_TESTS':""" >>> a = Person(name='Adrian', gender='M') >>> a.save() >>> s = Person(name='Sara', gender='F') @@ -36,4 +36,4 @@ API_TESTS = """ 'Male' >>> s.get_gender_display() 'Female' -""" +"""} diff --git a/tests/modeltests/custom_columns/models.py b/tests/modeltests/custom_columns/models.py index 7d8c52d137..e88fa80da2 100644 --- a/tests/modeltests/custom_columns/models.py +++ b/tests/modeltests/custom_columns/models.py @@ -15,7 +15,7 @@ class Person(models.Model): def __str__(self): return '%s %s' % (self.first_name, self.last_name) -API_TESTS = """ +__test__ = {'API_TESTS':""" # Create a Person. >>> p = Person(first_name='John', last_name='Smith') >>> p.save() @@ -50,4 +50,4 @@ AttributeError: 'Person' object has no attribute 'firstname' Traceback (most recent call last): ... AttributeError: 'Person' object has no attribute 'last' -""" +"""} diff --git a/tests/modeltests/custom_managers/models.py b/tests/modeltests/custom_managers/models.py index 1c4e91b526..99df875275 100644 --- a/tests/modeltests/custom_managers/models.py +++ b/tests/modeltests/custom_managers/models.py @@ -58,7 +58,7 @@ class Car(models.Model): def __str__(self): return self.name -API_TESTS = """ +__test__ = {'API_TESTS':""" >>> p1 = Person(first_name='Bugs', last_name='Bunny', fun=True) >>> p1.save() >>> p2 = Person(first_name='Droopy', last_name='Dog', fun=False) @@ -104,4 +104,4 @@ True # to the first manager defined in the class. In this case, it's "cars". >>> Car._default_manager.order_by('name') [, ] -""" +"""} diff --git a/tests/modeltests/custom_methods/models.py b/tests/modeltests/custom_methods/models.py index e314d97264..e8fb751d54 100644 --- a/tests/modeltests/custom_methods/models.py +++ b/tests/modeltests/custom_methods/models.py @@ -36,7 +36,7 @@ class Article(models.Model): # positional arguments to Article(). return [self.__class__(*row) for row in cursor.fetchall()] -API_TESTS = """ +__test__ = {'API_TESTS':""" # Create a couple of Articles. >>> from datetime import date >>> a = Article(id=None, headline='Area man programs in Python', pub_date=date(2005, 7, 27)) @@ -55,4 +55,4 @@ False [] >>> b.articles_from_same_day_2() [] -""" +"""} diff --git a/tests/modeltests/custom_pk/models.py b/tests/modeltests/custom_pk/models.py index f7b790ca21..ca788f6aa5 100644 --- a/tests/modeltests/custom_pk/models.py +++ b/tests/modeltests/custom_pk/models.py @@ -27,7 +27,7 @@ class Business(models.Model): def __str__(self): return self.name -API_TESTS = """ +__test__ = {'API_TESTS':""" >>> dan = Employee(employee_code='ABC123', first_name='Dan', last_name='Jones') >>> dan.save() >>> Employee.objects.all() @@ -88,4 +88,4 @@ DoesNotExist: Employee matching query does not exist. >>> Business.objects.filter(employees__first_name__startswith='Fran') [] -""" +"""} diff --git a/tests/modeltests/empty/models.py b/tests/modeltests/empty/models.py index 0f5a0b870f..0e5d572504 100644 --- a/tests/modeltests/empty/models.py +++ b/tests/modeltests/empty/models.py @@ -10,7 +10,7 @@ from django.db import models class Empty(models.Model): pass -API_TESTS = """ +__test__ = {'API_TESTS':""" >>> m = Empty() >>> m.id >>> m.save() @@ -23,4 +23,4 @@ True >>> existing = Empty(m.id) >>> existing.save() -""" +"""} diff --git a/tests/modeltests/field_defaults/models.py b/tests/modeltests/field_defaults/models.py index 0d69ffd8be..da4cd38974 100644 --- a/tests/modeltests/field_defaults/models.py +++ b/tests/modeltests/field_defaults/models.py @@ -19,7 +19,7 @@ class Article(models.Model): def __str__(self): return self.headline -API_TESTS = """ +__test__ = {'API_TESTS':""" >>> from datetime import datetime # No articles are in the system yet. @@ -48,4 +48,4 @@ API_TESTS = """ >>> d = now - a.pub_date >>> d.seconds < 5 True -""" +"""} diff --git a/tests/modeltests/generic_relations/models.py b/tests/modeltests/generic_relations/models.py index e9a81a19e8..eb64d7ec3d 100644 --- a/tests/modeltests/generic_relations/models.py +++ b/tests/modeltests/generic_relations/models.py @@ -53,7 +53,7 @@ class Mineral(models.Model): def __str__(self): return self.name -API_TESTS = """ +__test__ = {'API_TESTS':""" # Create the world in 7 lines of code... >>> lion = Animal(common_name="Lion", latin_name="Panthera leo") >>> platypus = Animal(common_name="Platypus", latin_name="Ornithorhynchus anatinus") @@ -105,4 +105,4 @@ API_TESTS = """ [] >>> TaggedItem.objects.filter(content_type__pk=ctype.id, object_id=quartz.id) [] -""" +"""} diff --git a/tests/modeltests/get_latest/models.py b/tests/modeltests/get_latest/models.py index 42e7a14ec7..84c6273818 100644 --- a/tests/modeltests/get_latest/models.py +++ b/tests/modeltests/get_latest/models.py @@ -3,9 +3,9 @@ Models can have a ``get_latest_by`` attribute, which should be set to the name of a DateField or DateTimeField. If ``get_latest_by`` exists, the model's -module will get a ``get_latest()`` function, which will return the latest -object in the database according to that field. "Latest" means "having the -date farthest into the future." +manager will get a ``latest()`` method, which will return the latest object in +the database according to that field. "Latest" means "having the date farthest +into the future." """ from django.db import models @@ -29,8 +29,8 @@ class Person(models.Model): def __str__(self): return self.name -API_TESTS = """ -# Because no Articles exist yet, get_latest() raises ArticleDoesNotExist. +__test__ = {'API_TESTS':""" +# Because no Articles exist yet, latest() raises ArticleDoesNotExist. >>> Article.objects.latest() Traceback (most recent call last): ... @@ -76,4 +76,4 @@ AssertionError: latest() requires either a field_name parameter or 'get_latest_b >>> Person.objects.latest('birthday') -""" +"""} diff --git a/tests/modeltests/get_or_create/models.py b/tests/modeltests/get_or_create/models.py index 10a8721afc..b4f39ceded 100644 --- a/tests/modeltests/get_or_create/models.py +++ b/tests/modeltests/get_or_create/models.py @@ -15,7 +15,7 @@ class Person(models.Model): def __str__(self): return '%s %s' % (self.first_name, self.last_name) -API_TESTS = """ +__test__ = {'API_TESTS':""" # Acting as a divine being, create an Person. >>> from datetime import date >>> p = Person(first_name='John', last_name='Lennon', birthday=date(1940, 10, 9)) @@ -49,4 +49,4 @@ True False >>> Person.objects.count() 2 -""" +"""} diff --git a/tests/modeltests/invalid_models/models.py b/tests/modeltests/invalid_models/models.py index eb305b4e92..5540c1bd5f 100644 --- a/tests/modeltests/invalid_models/models.py +++ b/tests/modeltests/invalid_models/models.py @@ -78,7 +78,7 @@ class SelfClashM2M(models.Model): -error_log = """invalid_models.fielderrors: "charfield": CharFields require a "maxlength" attribute. +model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "maxlength" attribute. invalid_models.fielderrors: "floatfield": FloatFields require a "decimal_places" attribute. invalid_models.fielderrors: "floatfield": FloatFields require a "max_digits" attribute. invalid_models.fielderrors: "filefield": FileFields require an "upload_to" attribute. diff --git a/tests/modeltests/lookup/models.py b/tests/modeltests/lookup/models.py index 55bb373a4b..09c3aa7aa8 100644 --- a/tests/modeltests/lookup/models.py +++ b/tests/modeltests/lookup/models.py @@ -15,7 +15,7 @@ class Article(models.Model): def __str__(self): return self.headline -API_TESTS = r""" +__test__ = {'API_TESTS':r""" # Create a couple of Articles. >>> from datetime import datetime >>> a1 = Article(headline='Article 1', pub_date=datetime(2005, 7, 26)) @@ -191,4 +191,4 @@ DoesNotExist: Article matching query does not exist. >>> Article.objects.filter(headline__contains='\\') [] -""" +"""} diff --git a/tests/modeltests/m2m_and_m2o/models.py b/tests/modeltests/m2m_and_m2o/models.py index f43fb12d9e..7fc66ed5a0 100644 --- a/tests/modeltests/m2m_and_m2o/models.py +++ b/tests/modeltests/m2m_and_m2o/models.py @@ -21,7 +21,7 @@ class Issue(models.Model): ordering = ('num',) -API_TESTS = """ +__test__ = {'API_TESTS':""" >>> Issue.objects.all() [] >>> r = User(username='russell') @@ -62,4 +62,4 @@ API_TESTS = """ [, , ] >>> Issue.objects.filter(Q(client=r.id) | Q(cc__id__exact=r.id)) [, , ] -""" +"""} diff --git a/tests/modeltests/m2m_intermediary/models.py b/tests/modeltests/m2m_intermediary/models.py index 848d035c39..b917db6189 100644 --- a/tests/modeltests/m2m_intermediary/models.py +++ b/tests/modeltests/m2m_intermediary/models.py @@ -34,7 +34,7 @@ class Writer(models.Model): def __str__(self): return '%s (%s)' % (self.reporter, self.position) -API_TESTS = """ +__test__ = {'API_TESTS':""" # Create a few Reporters. >>> r1 = Reporter(first_name='John', last_name='Smith') >>> r1.save() @@ -65,4 +65,4 @@ API_TESTS = """ >>> r1.writer_set.all() [] -""" +"""} diff --git a/tests/modeltests/m2m_multiple/models.py b/tests/modeltests/m2m_multiple/models.py index e4fef75f19..5a1aa122a9 100644 --- a/tests/modeltests/m2m_multiple/models.py +++ b/tests/modeltests/m2m_multiple/models.py @@ -28,7 +28,7 @@ class Article(models.Model): def __str__(self): return self.headline -API_TESTS = """ +__test__ = {'API_TESTS':""" >>> from datetime import datetime >>> c1 = Category(name='Sports') @@ -76,4 +76,4 @@ API_TESTS = """ [] >>> c4.secondary_article_set.all() [, ] -""" +"""} diff --git a/tests/modeltests/m2m_recursive/models.py b/tests/modeltests/m2m_recursive/models.py index dace32d565..9f31cf92c0 100644 --- a/tests/modeltests/m2m_recursive/models.py +++ b/tests/modeltests/m2m_recursive/models.py @@ -22,7 +22,7 @@ class Person(models.Model): def __str__(self): return self.name -API_TESTS = """ +__test__ = {'API_TESTS':""" >>> a = Person(name='Anne') >>> a.save() >>> b = Person(name='Bill') @@ -189,4 +189,4 @@ API_TESTS = """ >>> d.stalkers.all() [] -""" +"""} diff --git a/tests/modeltests/m2o_recursive/models.py b/tests/modeltests/m2o_recursive/models.py index 44881b5a2f..0b528faf9e 100644 --- a/tests/modeltests/m2o_recursive/models.py +++ b/tests/modeltests/m2o_recursive/models.py @@ -19,7 +19,7 @@ class Category(models.Model): def __str__(self): return self.name -API_TESTS = """ +__test__ = {'API_TESTS':""" # Create a few Category objects. >>> r = Category(id=None, name='Root category', parent=None) >>> r.save() @@ -37,4 +37,4 @@ None [] >>> c.parent -""" +"""} diff --git a/tests/modeltests/m2o_recursive2/models.py b/tests/modeltests/m2o_recursive2/models.py index 93185c6a4c..5b7c5447ad 100644 --- a/tests/modeltests/m2o_recursive2/models.py +++ b/tests/modeltests/m2o_recursive2/models.py @@ -17,7 +17,7 @@ class Person(models.Model): def __str__(self): return self.full_name -API_TESTS = """ +__test__ = {'API_TESTS':""" # Create two Person objects -- the mom and dad in our family. >>> dad = Person(full_name='John Smith Senior', mother=None, father=None) >>> dad.save() @@ -40,4 +40,4 @@ API_TESTS = """ [] >>> kid.fathers_child_set.all() [] -""" +"""} diff --git a/tests/modeltests/manipulators/models.py b/tests/modeltests/manipulators/models.py index f7b20d52ac..e5b8be55b5 100644 --- a/tests/modeltests/manipulators/models.py +++ b/tests/modeltests/manipulators/models.py @@ -21,7 +21,7 @@ class Album(models.Model): def __str__(self): return self.name -API_TESTS = """ +__test__ = {'API_TESTS':""" >>> from django.utils.datastructures import MultiValueDict # Create a Musician object via the default AddManipulator. @@ -88,4 +88,4 @@ True >>> a2.release_date datetime.date(2005, 2, 13) -""" +"""} diff --git a/tests/modeltests/many_to_many/models.py b/tests/modeltests/many_to_many/models.py index 0e989a0fbe..357f3ca629 100644 --- a/tests/modeltests/many_to_many/models.py +++ b/tests/modeltests/many_to_many/models.py @@ -28,7 +28,7 @@ class Article(models.Model): class Meta: ordering = ('headline',) -API_TESTS = """ +__test__ = {'API_TESTS':""" # Create a couple of Publications. >>> p1 = Publication(id=None, title='The Python Journal') >>> p1.save() @@ -231,4 +231,4 @@ API_TESTS = """ >>> p1.article_set.all() [] -""" +"""} diff --git a/tests/modeltests/many_to_one/models.py b/tests/modeltests/many_to_one/models.py index d202975128..82eb3257d0 100644 --- a/tests/modeltests/many_to_one/models.py +++ b/tests/modeltests/many_to_one/models.py @@ -25,7 +25,7 @@ class Article(models.Model): class Meta: ordering = ('headline',) -API_TESTS = """ +__test__ = {'API_TESTS':""" # Create a few Reporters. >>> r = Reporter(first_name='John', last_name='Smith', email='john@example.com') >>> r.save() @@ -263,4 +263,4 @@ TypeError: Cannot resolve keyword 'reporter_id' into field >>> Article.objects.all() [] -""" +"""} diff --git a/tests/modeltests/many_to_one_null/models.py b/tests/modeltests/many_to_one_null/models.py index b1936b9861..fb0f6ac3b7 100644 --- a/tests/modeltests/many_to_one_null/models.py +++ b/tests/modeltests/many_to_one_null/models.py @@ -23,7 +23,7 @@ class Article(models.Model): def __str__(self): return self.headline -API_TESTS = """ +__test__ = {'API_TESTS':""" # Create a Reporter. >>> r = Reporter(name='John Smith') >>> r.save() @@ -121,4 +121,4 @@ DoesNotExist: is not related to . >>> Article.objects.filter(reporter__isnull=True) [, ] -""" +"""} diff --git a/tests/modeltests/model_inheritance/models.py b/tests/modeltests/model_inheritance/models.py index 473cf24a9f..babef73e0a 100644 --- a/tests/modeltests/model_inheritance/models.py +++ b/tests/modeltests/model_inheritance/models.py @@ -26,7 +26,7 @@ class ItalianRestaurant(Restaurant): def __str__(self): return "%s the italian restaurant" % self.name -API_TESTS = """ +__test__ = {'API_TESTS':""" # Make sure Restaurant has the right fields in the right order. >>> [f.name for f in Restaurant._meta.fields] ['id', 'name', 'address', 'serves_hot_dogs', 'serves_pizza'] @@ -50,4 +50,4 @@ API_TESTS = """ >>> ir.save() -""" +"""} diff --git a/tests/modeltests/multiple_databases/models.py b/tests/modeltests/multiple_databases/models.py index ce52d96f0f..ce0be8f6c1 100644 --- a/tests/modeltests/multiple_databases/models.py +++ b/tests/modeltests/multiple_databases/models.py @@ -72,26 +72,25 @@ class Vehicle(models.Model): return "%d %s %s" % (self.year, self.make, self.model) -API_TESTS = """ +__test__ = {'API_TESTS': """ # See what connections are defined. django.db.connections acts like a dict. ->>> from django.db import connection, connections, _default +>>> from django.db import connection, connections, _default, model_connection_name >>> from django.conf import settings # The default connection is in there, but let's ignore it >>> non_default = connections.keys() >>> non_default.remove(_default) +>>> non_default.sort() >>> non_default -['django_test_db_a', 'django_test_db_b'] +['_a', '_b'] # Each connection references its settings ->>> connections['django_test_db_a'].settings.DATABASE_NAME == settings.OTHER_DATABASES['django_test_db_a']['DATABASE_NAME'] +>>> connections['_a'].settings.DATABASE_NAME == settings.OTHER_DATABASES['_a']['DATABASE_NAME'] True ->>> connections['django_test_db_b'].settings.DATABASE_NAME == settings.OTHER_DATABASES['django_test_db_b']['DATABASE_NAME'] +>>> connections['_b'].settings.DATABASE_NAME == settings.OTHER_DATABASES['_b']['DATABASE_NAME'] True ->>> connections['django_test_db_b'].settings.DATABASE_NAME == settings.OTHER_DATABASES['django_test_db_a']['DATABASE_NAME'] -False # Invalid connection names raise ImproperlyConfigured >>> connections['bad'] @@ -100,16 +99,12 @@ Traceback (most recent call last): ImproperlyConfigured: No database connection 'bad' has been configured # Models can access their connections through their managers ->>> Artist.objects.db is connections['django_test_db_a'] +>>> model_connection_name(Artist) +'_a' +>>> model_connection_name(Widget) +'_b' +>>> model_connection_name(Vehicle) is _default True ->>> Widget.objects.db is connections['django_test_db_b'] -True ->>> Vehicle.objects.db.connection.settings.DATABASE_NAME == connection.settings.DATABASE_NAME -True ->>> Artist.objects.db is Widget.objects.db -False ->>> Artist.objects.db.connection is Vehicle.objects.db.connection -False >>> a = Artist(name="Paul Klee", alive=False) >>> a.save() >>> w = Widget(code='100x2r', weight=1000) @@ -130,7 +125,7 @@ False >>> w = Widget(code="99rbln", weight=1) >>> a.save() -# Only connection 'django_test_db_a' is committed, so if we rollback +# Only connection '_a' is committed, so if we rollback # all connections we'll forget the new Widget. >>> transaction.rollback() @@ -162,11 +157,11 @@ False # dict is passed, the keys are ignored and the values used as the list # of connections to commit, rollback, etc. ->>> transaction.commit(connections['django_test_db_b']) ->>> transaction.commit('django_test_db_b') ->>> transaction.commit(connections='django_test_db_b') ->>> transaction.commit(connections=['django_test_db_b']) ->>> transaction.commit(['django_test_db_a', 'django_test_db_b']) +>>> transaction.commit(connections['_b']) +>>> transaction.commit('_b') +>>> transaction.commit(connections='_b') +>>> transaction.commit(connections=['_b']) +>>> transaction.commit(['_a', '_b']) >>> transaction.commit(connections) # When the connections argument is omitted entirely, the transaction @@ -211,4 +206,4 @@ False >>> w = Widget.objects.get(code='d101') >>> list(w.doohickeys.all()) [] -""" +"""} diff --git a/tests/modeltests/mutually_referential/models.py b/tests/modeltests/mutually_referential/models.py index 07b52effbc..5a3897d21a 100644 --- a/tests/modeltests/mutually_referential/models.py +++ b/tests/modeltests/mutually_referential/models.py @@ -14,7 +14,7 @@ class Child(Model): name = CharField(maxlength=100) parent = ForeignKey(Parent) -API_TESTS = """ +__test__ = {'API_TESTS':""" # Create a Parent >>> q = Parent(name='Elizabeth') >>> q.save() @@ -29,4 +29,4 @@ API_TESTS = """ >>> q.delete() -""" \ No newline at end of file +"""} \ No newline at end of file diff --git a/tests/modeltests/one_to_one/models.py b/tests/modeltests/one_to_one/models.py index f95556c08d..8afa74454d 100644 --- a/tests/modeltests/one_to_one/models.py +++ b/tests/modeltests/one_to_one/models.py @@ -30,7 +30,7 @@ class Waiter(models.Model): def __str__(self): return "%s the waiter at %s" % (self.name, self.restaurant) -API_TESTS = """ +__test__ = {'API_TESTS':""" # Create a couple of Places. >>> p1 = Place(name='Demon Dogs', address='944 W. Fullerton') >>> p1.save() @@ -151,4 +151,4 @@ DoesNotExist: Restaurant matching query does not exist. # Delete the restaurant; the waiter should also be removed >>> r = Restaurant.objects.get(pk=1) >>> r.delete() -""" +"""} diff --git a/tests/modeltests/or_lookups/models.py b/tests/modeltests/or_lookups/models.py index 80dc35f2b1..2de18edc1f 100644 --- a/tests/modeltests/or_lookups/models.py +++ b/tests/modeltests/or_lookups/models.py @@ -23,7 +23,7 @@ class Article(models.Model): def __str__(self): return self.headline -API_TESTS = """ +__test__ = {'API_TESTS':""" >>> from datetime import datetime >>> from django.db.models import Q @@ -101,4 +101,4 @@ API_TESTS = """ [] >>> Article.objects.complex_filter(Q(pk=1) | Q(pk=2)) [, ] -""" +"""} diff --git a/tests/modeltests/ordering/models.py b/tests/modeltests/ordering/models.py index b22568c900..110ae3d7fc 100644 --- a/tests/modeltests/ordering/models.py +++ b/tests/modeltests/ordering/models.py @@ -24,7 +24,7 @@ class Article(models.Model): def __str__(self): return self.headline -API_TESTS = """ +__test__ = {'API_TESTS':""" # Create a couple of Articles. >>> from datetime import datetime >>> a1 = Article(headline='Article 1', pub_date=datetime(2005, 7, 26)) @@ -64,4 +64,4 @@ API_TESTS = """ # don't know what order the output will be in. >>> Article.objects.order_by('?') [...] -""" +"""} diff --git a/tests/modeltests/pagination/models.py b/tests/modeltests/pagination/models.py index 165b251d35..ea2385dc79 100644 --- a/tests/modeltests/pagination/models.py +++ b/tests/modeltests/pagination/models.py @@ -15,7 +15,7 @@ class Article(models.Model): def __str__(self): return self.headline -API_TESTS = """ +__test__ = {'API_TESTS':""" # prepare a list of objects for pagination >>> from datetime import datetime >>> for x in range(1, 10): @@ -64,4 +64,4 @@ True >>> paginator.last_on_page(1) 9 -""" +"""} diff --git a/tests/modeltests/properties/models.py b/tests/modeltests/properties/models.py index 3b0133bf8a..4ba6b1a808 100644 --- a/tests/modeltests/properties/models.py +++ b/tests/modeltests/properties/models.py @@ -20,7 +20,7 @@ class Person(models.Model): full_name_2 = property(_get_full_name, _set_full_name) -API_TESTS = """ +__test__ = {'API_TESTS':""" >>> a = Person(first_name='John', last_name='Lennon') >>> a.save() >>> a.full_name @@ -37,4 +37,4 @@ AttributeError: can't set attribute >>> a2.save() >>> a2.first_name 'Paul' -""" +"""} diff --git a/tests/modeltests/reserved_names/models.py b/tests/modeltests/reserved_names/models.py index db9196bebe..affe3f649d 100644 --- a/tests/modeltests/reserved_names/models.py +++ b/tests/modeltests/reserved_names/models.py @@ -24,7 +24,7 @@ class Thing(models.Model): def __str__(self): return self.when -API_TESTS = """ +__test__ = {'API_TESTS':""" >>> import datetime >>> day1 = datetime.date(2005, 1, 1) >>> day2 = datetime.date(2006, 2, 2) @@ -53,4 +53,4 @@ b >>> Thing.objects.filter(where__month=1) [] -""" +"""} diff --git a/tests/modeltests/reverse_lookup/models.py b/tests/modeltests/reverse_lookup/models.py index b8c4466021..7e6712676f 100644 --- a/tests/modeltests/reverse_lookup/models.py +++ b/tests/modeltests/reverse_lookup/models.py @@ -27,7 +27,7 @@ class Choice(models.Model): def __str(self): return self.name -API_TESTS = """ +__test__ = {'API_TESTS':""" >>> john = User(name="John Doe") >>> john.save() >>> jim = User(name="Jim Bo") @@ -56,4 +56,4 @@ API_TESTS = """ Traceback (most recent call last): ... TypeError: Cannot resolve keyword 'choice' into field -""" +"""} diff --git a/tests/modeltests/save_delete_hooks/models.py b/tests/modeltests/save_delete_hooks/models.py index f01a2932d3..6e24c308ba 100644 --- a/tests/modeltests/save_delete_hooks/models.py +++ b/tests/modeltests/save_delete_hooks/models.py @@ -24,7 +24,7 @@ class Person(models.Model): super(Person, self).delete() # Call the "real" delete() method print "After deletion" -API_TESTS = """ +__test__ = {'API_TESTS':""" >>> p1 = Person(first_name='John', last_name='Smith') >>> p1.save() Before save @@ -39,4 +39,4 @@ After deletion >>> Person.objects.all() [] -""" +"""} diff --git a/tests/modeltests/serializers/models.py b/tests/modeltests/serializers/models.py index ccf565c365..d1d10b43c0 100644 --- a/tests/modeltests/serializers/models.py +++ b/tests/modeltests/serializers/models.py @@ -37,7 +37,7 @@ class Article(models.Model): def __str__(self): return self.headline -API_TESTS = """ +__test__ = {'API_TESTS':""" # Create some data: >>> from datetime import datetime >>> sports = Category(name="Sports") @@ -118,4 +118,4 @@ API_TESTS = """ >>> Article.objects.all() [, ] -""" +"""} diff --git a/tests/modeltests/str/models.py b/tests/modeltests/str/models.py index 4e4228ac89..81230d538c 100644 --- a/tests/modeltests/str/models.py +++ b/tests/modeltests/str/models.py @@ -17,7 +17,7 @@ class Article(models.Model): def __str__(self): return self.headline -API_TESTS = """ +__test__ = {'API_TESTS':""" # Create an Article. >>> from datetime import datetime >>> a = Article(headline='Area man programs in Python', pub_date=datetime(2005, 7, 28)) @@ -28,4 +28,4 @@ API_TESTS = """ >>> a -""" +"""} diff --git a/tests/modeltests/transactions/models.py b/tests/modeltests/transactions/models.py index 92e0f38f47..e1fad8063e 100644 --- a/tests/modeltests/transactions/models.py +++ b/tests/modeltests/transactions/models.py @@ -17,16 +17,16 @@ class Reporter(models.Model): def __str__(self): return "%s %s" % (self.first_name, self.last_name) -API_TESTS = """ +__test__ = {'API_TESTS':""" >>> from django.db import connection, transaction -""" +"""} from django.conf import settings building_docs = getattr(settings, 'BUILDING_DOCS', False) if building_docs or settings.DATABASE_ENGINE != 'mysql': - API_TESTS += """ + __test__['API_TESTS'] += """ # the default behavior is to autocommit after each save() action >>> def create_a_reporter_then_fail(first, last): ... a = Reporter(first_name=first, last_name=last) diff --git a/tests/modeltests/validation/models.py b/tests/modeltests/validation/models.py index 57811f25a5..a9a3d3f485 100644 --- a/tests/modeltests/validation/models.py +++ b/tests/modeltests/validation/models.py @@ -20,7 +20,7 @@ class Person(models.Model): def __str__(self): return self.name -API_TESTS = """ +__test__ = {'API_TESTS':""" >>> import datetime >>> valid_params = { @@ -146,4 +146,4 @@ u'john@example.com' >>> p.validate() {'email': ['Enter a valid e-mail address.']} -""" +"""} diff --git a/tests/othertests/cache.py b/tests/othertests/cache.py deleted file mode 100644 index 81f2c20328..0000000000 --- a/tests/othertests/cache.py +++ /dev/null @@ -1,60 +0,0 @@ -# Unit tests for cache framework -# Uses whatever cache backend is set in the test settings file. - -from django.core.cache import cache -import time - -# functions/classes for complex data type tests -def f(): - return 42 -class C: - def m(n): - return 24 - -# simple set/get -cache.set("key", "value") -assert cache.get("key") == "value" - -# get with non-existent keys -assert cache.get("does not exist") is None -assert cache.get("does not exist", "bang!") == "bang!" - -# get_many -cache.set('a', 'a') -cache.set('b', 'b') -cache.set('c', 'c') -cache.set('d', 'd') -assert cache.get_many(['a', 'c', 'd']) == {'a' : 'a', 'c' : 'c', 'd' : 'd'} -assert cache.get_many(['a', 'b', 'e']) == {'a' : 'a', 'b' : 'b'} - -# delete -cache.set("key1", "spam") -cache.set("key2", "eggs") -assert cache.get("key1") == "spam" -cache.delete("key1") -assert cache.get("key1") is None -assert cache.get("key2") == "eggs" - -# has_key -cache.set("hello", "goodbye") -assert cache.has_key("hello") == True -assert cache.has_key("goodbye") == False - -# test data types -stuff = { - 'string' : 'this is a string', - 'int' : 42, - 'list' : [1, 2, 3, 4], - 'tuple' : (1, 2, 3, 4), - 'dict' : {'A': 1, 'B' : 2}, - 'function' : f, - 'class' : C, -} -for (key, value) in stuff.items(): - cache.set(key, value) - assert cache.get(key) == value - -# expiration -cache.set('expire', 'very quickly', 1) -time.sleep(2) -assert cache.get("expire") == None diff --git a/tests/othertests/manager_db.py b/tests/othertests/manager_db.py deleted file mode 100644 index 936b6a21d5..0000000000 --- a/tests/othertests/manager_db.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.db import models - -def run_tests(verbosity=0): - class Insect(models.Model): - common_name = models.CharField(maxlength=64) - latin_name = models.CharField(maxlength=128) - - class Meta: - app_label = 'manager_db' - - m = Insect.objects - db = Insect.objects.db - - assert db - assert db.connection - assert db.connection.cursor - assert db.backend - assert db.backend.quote_name - assert db.get_creation_module - -if __name__ == '__main__': - run_tests() - print "ok" diff --git a/tests/othertests/markup.py b/tests/othertests/markup.py deleted file mode 100644 index 2b00a8c7a5..0000000000 --- a/tests/othertests/markup.py +++ /dev/null @@ -1,70 +0,0 @@ -# Quick tests for the markup templatetags (django.contrib.markup) - -from django.template import Template, Context, add_to_builtins -import re - -add_to_builtins('django.contrib.markup.templatetags.markup') - -# find out if markup modules are installed and tailor the test appropriately -try: - import textile -except ImportError: - textile = None - -try: - import markdown -except ImportError: - markdown = None - -try: - import docutils -except ImportError: - docutils = None - -# simple examples 'cause this isn't actually testing the markup, just -# that the filters work as advertised - -### test textile - -textile_content = """Paragraph 1 - -Paragraph 2 with "quotes" and @code@""" - -t = Template("{{ textile_content|textile }}") -rendered = t.render(Context(locals())).strip() -if textile: - assert rendered == """

Paragraph 1

- -

Paragraph 2 with “quotes” and code

""" -else: - assert rendered == textile_content - -### test markdown - -markdown_content = """Paragraph 1 - -## An h2""" - -t = Template("{{ markdown_content|markdown }}") -rendered = t.render(Context(locals())).strip() -if markdown: - pattern = re.compile("""

Paragraph 1\s*

\s*

\s*An h2

""") - assert pattern.match(rendered) -else: - assert rendered == markdown_content - -### test rest - -rest_content = """Paragraph 1 - -Paragraph 2 with a link_ - -.. _link: http://www.example.com/""" - -t = Template("{{ rest_content|restructuredtext }}") -rendered = t.render(Context(locals())).strip() -if docutils: - assert rendered =="""

Paragraph 1

-

Paragraph 2 with a link

""" -else: - assert rendered == rest_content diff --git a/tests/othertests/request_isolation.py b/tests/othertests/request_isolation.py deleted file mode 100644 index 6c29306c4e..0000000000 --- a/tests/othertests/request_isolation.py +++ /dev/null @@ -1,140 +0,0 @@ -# tests that db settings can change between requests -import copy -import os -from django.conf import settings, UserSettingsHolder -from django.core.handlers.wsgi import WSGIHandler -from django.db import models, model_connection_name, _default, connection -from django.http import HttpResponse - -# state holder -S = {} - -# models -class MX(models.Model): - val = models.CharField(maxlength=10) - class Meta: - app_label = 'ri' - - -class MY(models.Model): - val = models.CharField(maxlength=10) - class Meta: - app_label = 'ri' - - -# tests -def test_one(path, request): - """Start out with settings as originally configured""" - assert model_connection_name(MX) == 'django_test_db_a' - assert MX._default_manager.db.connection.settings.DATABASE_NAME == \ - settings.OTHER_DATABASES['django_test_db_a']['DATABASE_NAME'] - assert model_connection_name(MY) == 'django_test_db_b' - assert MY._default_manager.db.connection.settings.DATABASE_NAME == \ - settings.OTHER_DATABASES['django_test_db_b']['DATABASE_NAME'], \ - "%s != %s" % \ - (MY._default_manager.db.connection.settings.DATABASE_NAME, - settings.OTHER_DATABASES['django_test_db_b']['DATABASE_NAME']) - - -def test_two(path, request): - """Between the first and second requests, settings change to assign - model MY to a different connection - """ - assert model_connection_name(MX) == 'django_test_db_a' - assert MX._default_manager.db.connection.settings.DATABASE_NAME == \ - settings.OTHER_DATABASES['django_test_db_a']['DATABASE_NAME'] - assert model_connection_name(MY) == _default - assert MY._default_manager.db.connection.settings.DATABASE_NAME == \ - settings.DATABASE_NAME, "%s != %s" % \ - (MY._default_manager.db.connection.settings.DATABASE_NAME, - settings.DATABASE_NAME) - - -def test_three(path, request): - """Between the 2nd and 3rd requests, the settings at the names in - OTHER_DATABASES have changed. - """ - assert model_connection_name(MX) == 'django_test_db_b' - assert MX._default_manager.db.connection.settings.DATABASE_NAME == \ - settings.OTHER_DATABASES['django_test_db_b']['DATABASE_NAME'] - assert model_connection_name(MY) == 'django_test_db_a' - assert MY._default_manager.db.connection.settings.DATABASE_NAME == \ - settings.OTHER_DATABASES['django_test_db_a']['DATABASE_NAME'], \ - "%s != %s" % \ - (MY._default_manager.db.connection.settings.DATABASE_NAME, - settings.OTHER_DATABASES['django_test_db_a']['DATABASE_NAME']) - - -# helpers -class MockHandler(WSGIHandler): - - def __init__(self, test): - self.test = test - super(MockHandler, self).__init__() - - def get_response(self, path, request): - # debug("mock handler answering %s, %s", path, request) - return HttpResponse(self.test(path, request)) - - -def pr(*arg): - if S['verbosity'] >= 1: - msg, arg = arg[0], arg[1:] - print msg % arg - - -def debug(*arg): - if S['verbosity'] >= 2: - msg, arg = arg[0], arg[1:] - print msg % arg - - -def setup(): - debug("setup") - S['settings'] = settings._target - settings._target = UserSettingsHolder(copy.deepcopy(settings._target)) - settings.OTHER_DATABASES['django_test_db_a']['MODELS'] = ['ri.MX'] - settings.OTHER_DATABASES['django_test_db_b']['MODELS'] = ['ri.MY'] - - -def teardown(): - debug("teardown") - settings._target = S['settings'] - - -def start_response(code, headers): - debug("start response: %s %s", code, headers) - pass - - -def main(): - debug("running tests") - - env = os.environ.copy() - env['PATH_INFO'] = '/' - env['REQUEST_METHOD'] = 'GET' - - MockHandler(test_one)(env, start_response) - - settings.OTHER_DATABASES['django_test_db_b']['MODELS'] = [] - MockHandler(test_two)(env, start_response) - - settings.OTHER_DATABASES['django_test_db_b']['MODELS'] = ['ri.MY'] - settings.OTHER_DATABASES['django_test_db_b'], \ - settings.OTHER_DATABASES['django_test_db_a'] = \ - settings.OTHER_DATABASES['django_test_db_a'], \ - settings.OTHER_DATABASES['django_test_db_b'] - MockHandler(test_three)(env, start_response) - - -def run_tests(verbosity=0): - S['verbosity'] = verbosity - setup() - try: - main() - finally: - teardown() - - -if __name__ == '__main__': - run_tests(2) diff --git a/tests/othertests/templates.py b/tests/othertests/templates.py deleted file mode 100644 index 9975f3b05c..0000000000 --- a/tests/othertests/templates.py +++ /dev/null @@ -1,634 +0,0 @@ -from django.conf import settings - -if __name__ == '__main__': - # When running this file in isolation, we need to set up the configuration - # before importing 'template'. - settings.configure() - -from django import template -from django.template import loader -from django.utils.translation import activate, deactivate, install -from django.utils.tzinfo import LocalTimezone -from datetime import datetime, timedelta -import traceback - -################################# -# Custom template tag for tests # -################################# - -register = template.Library() - -class EchoNode(template.Node): - def __init__(self, contents): - self.contents = contents - - def render(self, context): - return " ".join(self.contents) - -def do_echo(parser, token): - return EchoNode(token.contents.split()[1:]) - -register.tag("echo", do_echo) - -template.libraries['django.templatetags.testtags'] = register - -##################################### -# Helper objects for template tests # -##################################### - -class SomeException(Exception): - silent_variable_failure = True - -class SomeOtherException(Exception): - pass - -class SomeClass: - def __init__(self): - self.otherclass = OtherClass() - - def method(self): - return "SomeClass.method" - - def method2(self, o): - return o - - def method3(self): - raise SomeException - - def method4(self): - raise SomeOtherException - -class OtherClass: - def method(self): - return "OtherClass.method" - -# NOW and NOW_tz are used by timesince tag tests. -NOW = datetime.now() -NOW_tz = datetime.now(LocalTimezone(datetime.now())) - -# SYNTAX -- -# 'template_name': ('template contents', 'context dict', 'expected string output' or Exception class) -TEMPLATE_TESTS = { - - ### BASIC SYNTAX ########################################################## - - # Plain text should go through the template parser untouched - 'basic-syntax01': ("something cool", {}, "something cool"), - - # Variables should be replaced with their value in the current context - 'basic-syntax02': ("{{ headline }}", {'headline':'Success'}, "Success"), - - # More than one replacement variable is allowed in a template - 'basic-syntax03': ("{{ first }} --- {{ second }}", {"first" : 1, "second" : 2}, "1 --- 2"), - - # Fail silently when a variable is not found in the current context - 'basic-syntax04': ("as{{ missing }}df", {}, "asINVALIDdf"), - - # A variable may not contain more than one word - 'basic-syntax06': ("{{ multi word variable }}", {}, template.TemplateSyntaxError), - - # Raise TemplateSyntaxError for empty variable tags - 'basic-syntax07': ("{{ }}", {}, template.TemplateSyntaxError), - 'basic-syntax08': ("{{ }}", {}, template.TemplateSyntaxError), - - # Attribute syntax allows a template to call an object's attribute - 'basic-syntax09': ("{{ var.method }}", {"var": SomeClass()}, "SomeClass.method"), - - # Multiple levels of attribute access are allowed - 'basic-syntax10': ("{{ var.otherclass.method }}", {"var": SomeClass()}, "OtherClass.method"), - - # Fail silently when a variable's attribute isn't found - 'basic-syntax11': ("{{ var.blech }}", {"var": SomeClass()}, "INVALID"), - - # Raise TemplateSyntaxError when trying to access a variable beginning with an underscore - 'basic-syntax12': ("{{ var.__dict__ }}", {"var": SomeClass()}, template.TemplateSyntaxError), - - # Raise TemplateSyntaxError when trying to access a variable containing an illegal character - 'basic-syntax13': ("{{ va>r }}", {}, template.TemplateSyntaxError), - 'basic-syntax14': ("{{ (var.r) }}", {}, template.TemplateSyntaxError), - 'basic-syntax15': ("{{ sp%am }}", {}, template.TemplateSyntaxError), - 'basic-syntax16': ("{{ eggs! }}", {}, template.TemplateSyntaxError), - 'basic-syntax17': ("{{ moo? }}", {}, template.TemplateSyntaxError), - - # Attribute syntax allows a template to call a dictionary key's value - 'basic-syntax18': ("{{ foo.bar }}", {"foo" : {"bar" : "baz"}}, "baz"), - - # Fail silently when a variable's dictionary key isn't found - 'basic-syntax19': ("{{ foo.spam }}", {"foo" : {"bar" : "baz"}}, "INVALID"), - - # Fail silently when accessing a non-simple method - 'basic-syntax20': ("{{ var.method2 }}", {"var": SomeClass()}, "INVALID"), - - # Basic filter usage - 'basic-syntax21': ("{{ var|upper }}", {"var": "Django is the greatest!"}, "DJANGO IS THE GREATEST!"), - - # Chained filters - 'basic-syntax22': ("{{ var|upper|lower }}", {"var": "Django is the greatest!"}, "django is the greatest!"), - - # Raise TemplateSyntaxError for space between a variable and filter pipe - 'basic-syntax23': ("{{ var |upper }}", {}, template.TemplateSyntaxError), - - # Raise TemplateSyntaxError for space after a filter pipe - 'basic-syntax24': ("{{ var| upper }}", {}, template.TemplateSyntaxError), - - # Raise TemplateSyntaxError for a nonexistent filter - 'basic-syntax25': ("{{ var|does_not_exist }}", {}, template.TemplateSyntaxError), - - # Raise TemplateSyntaxError when trying to access a filter containing an illegal character - 'basic-syntax26': ("{{ var|fil(ter) }}", {}, template.TemplateSyntaxError), - - # Raise TemplateSyntaxError for invalid block tags - 'basic-syntax27': ("{% nothing_to_see_here %}", {}, template.TemplateSyntaxError), - - # Raise TemplateSyntaxError for empty block tags - 'basic-syntax28': ("{% %}", {}, template.TemplateSyntaxError), - - # Chained filters, with an argument to the first one - 'basic-syntax29': ('{{ var|removetags:"b i"|upper|lower }}', {"var": "Yes"}, "yes"), - - # Escaped string as argument - 'basic-syntax30': (r'{{ var|default_if_none:" endquote\" hah" }}', {"var": None}, ' endquote" hah'), - - # Variable as argument - 'basic-syntax31': (r'{{ var|default_if_none:var2 }}', {"var": None, "var2": "happy"}, 'happy'), - - # Default argument testing - 'basic-syntax32': (r'{{ var|yesno:"yup,nup,mup" }} {{ var|yesno }}', {"var": True}, 'yup yes'), - - # Fail silently for methods that raise an exception with a "silent_variable_failure" attribute - 'basic-syntax33': (r'1{{ var.method3 }}2', {"var": SomeClass()}, "1INVALID2"), - - # In methods that raise an exception without a "silent_variable_attribute" set to True, - # the exception propogates - 'basic-syntax34': (r'1{{ var.method4 }}2', {"var": SomeClass()}, SomeOtherException), - - # Escaped backslash in argument - 'basic-syntax35': (r'{{ var|default_if_none:"foo\bar" }}', {"var": None}, r'foo\bar'), - - # Escaped backslash using known escape char - 'basic-syntax35': (r'{{ var|default_if_none:"foo\now" }}', {"var": None}, r'foo\now'), - - ### COMMENT TAG ########################################################### - 'comment-tag01': ("{% comment %}this is hidden{% endcomment %}hello", {}, "hello"), - 'comment-tag02': ("{% comment %}this is hidden{% endcomment %}hello{% comment %}foo{% endcomment %}", {}, "hello"), - - # Comment tag can contain invalid stuff. - 'comment-tag03': ("foo{% comment %} {% if %} {% endcomment %}", {}, "foo"), - 'comment-tag04': ("foo{% comment %} {% endblock %} {% endcomment %}", {}, "foo"), - 'comment-tag05': ("foo{% comment %} {% somerandomtag %} {% endcomment %}", {}, "foo"), - - ### CYCLE TAG ############################################################# - 'cycle01': ('{% cycle a %}', {}, template.TemplateSyntaxError), - 'cycle02': ('{% cycle a,b,c as abc %}{% cycle abc %}', {}, 'ab'), - 'cycle03': ('{% cycle a,b,c as abc %}{% cycle abc %}{% cycle abc %}', {}, 'abc'), - 'cycle04': ('{% cycle a,b,c as abc %}{% cycle abc %}{% cycle abc %}{% cycle abc %}', {}, 'abca'), - 'cycle05': ('{% cycle %}', {}, template.TemplateSyntaxError), - 'cycle06': ('{% cycle a %}', {}, template.TemplateSyntaxError), - 'cycle07': ('{% cycle a,b,c as foo %}{% cycle bar %}', {}, template.TemplateSyntaxError), - - ### EXCEPTIONS ############################################################ - - # Raise exception for invalid template name - 'exception01': ("{% extends 'nonexistent' %}", {}, template.TemplateSyntaxError), - - # Raise exception for invalid template name (in variable) - 'exception02': ("{% extends nonexistent %}", {}, template.TemplateSyntaxError), - - # Raise exception for extra {% extends %} tags - 'exception03': ("{% extends 'inheritance01' %}{% block first %}2{% endblock %}{% extends 'inheritance16' %}", {}, template.TemplateSyntaxError), - - # Raise exception for custom tags used in child with {% load %} tag in parent, not in child - 'exception04': ("{% extends 'inheritance17' %}{% block first %}{% echo 400 %}5678{% endblock %}", {}, template.TemplateSyntaxError), - - ### FILTER TAG ############################################################ - 'filter01': ('{% filter upper %}{% endfilter %}', {}, ''), - 'filter02': ('{% filter upper %}django{% endfilter %}', {}, 'DJANGO'), - 'filter03': ('{% filter upper|lower %}django{% endfilter %}', {}, 'django'), - - ### FIRSTOF TAG ########################################################### - 'firstof01': ('{% firstof a b c %}', {'a':0,'b':0,'c':0}, ''), - 'firstof02': ('{% firstof a b c %}', {'a':1,'b':0,'c':0}, '1'), - 'firstof03': ('{% firstof a b c %}', {'a':0,'b':2,'c':0}, '2'), - 'firstof04': ('{% firstof a b c %}', {'a':0,'b':0,'c':3}, '3'), - 'firstof05': ('{% firstof a b c %}', {'a':1,'b':2,'c':3}, '1'), - 'firstof06': ('{% firstof %}', {}, template.TemplateSyntaxError), - - ### FOR TAG ############################################################### - 'for-tag01': ("{% for val in values %}{{ val }}{% endfor %}", {"values": [1, 2, 3]}, "123"), - 'for-tag02': ("{% for val in values reversed %}{{ val }}{% endfor %}", {"values": [1, 2, 3]}, "321"), - 'for-tag-vars01': ("{% for val in values %}{{ forloop.counter }}{% endfor %}", {"values": [6, 6, 6]}, "123"), - 'for-tag-vars02': ("{% for val in values %}{{ forloop.counter0 }}{% endfor %}", {"values": [6, 6, 6]}, "012"), - 'for-tag-vars03': ("{% for val in values %}{{ forloop.revcounter }}{% endfor %}", {"values": [6, 6, 6]}, "321"), - 'for-tag-vars04': ("{% for val in values %}{{ forloop.revcounter0 }}{% endfor %}", {"values": [6, 6, 6]}, "210"), - - ### IF TAG ################################################################ - 'if-tag01': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": True}, "yes"), - 'if-tag02': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": False}, "no"), - 'if-tag03': ("{% if foo %}yes{% else %}no{% endif %}", {}, "no"), - - # AND - 'if-tag-and01': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'yes'), - 'if-tag-and02': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'no'), - 'if-tag-and03': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'no'), - 'if-tag-and04': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'no'), - 'if-tag-and05': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': False}, 'no'), - 'if-tag-and06': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'bar': False}, 'no'), - 'if-tag-and07': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': True}, 'no'), - 'if-tag-and08': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'bar': True}, 'no'), - - # OR - 'if-tag-or01': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'yes'), - 'if-tag-or02': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'yes'), - 'if-tag-or03': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'yes'), - 'if-tag-or04': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'no'), - 'if-tag-or05': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': False}, 'no'), - 'if-tag-or06': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'bar': False}, 'no'), - 'if-tag-or07': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': True}, 'yes'), - 'if-tag-or08': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'bar': True}, 'yes'), - - # TODO: multiple ORs - - # NOT - 'if-tag-not01': ("{% if not foo %}no{% else %}yes{% endif %}", {'foo': True}, 'yes'), - 'if-tag-not02': ("{% if not %}yes{% else %}no{% endif %}", {'foo': True}, 'no'), - 'if-tag-not03': ("{% if not %}yes{% else %}no{% endif %}", {'not': True}, 'yes'), - 'if-tag-not04': ("{% if not not %}no{% else %}yes{% endif %}", {'not': True}, 'yes'), - 'if-tag-not05': ("{% if not not %}no{% else %}yes{% endif %}", {}, 'no'), - - 'if-tag-not06': ("{% if foo and not bar %}yes{% else %}no{% endif %}", {}, 'no'), - 'if-tag-not07': ("{% if foo and not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'no'), - 'if-tag-not08': ("{% if foo and not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'yes'), - 'if-tag-not09': ("{% if foo and not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'no'), - 'if-tag-not10': ("{% if foo and not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'no'), - - 'if-tag-not11': ("{% if not foo and bar %}yes{% else %}no{% endif %}", {}, 'no'), - 'if-tag-not12': ("{% if not foo and bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'no'), - 'if-tag-not13': ("{% if not foo and bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'no'), - 'if-tag-not14': ("{% if not foo and bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'yes'), - 'if-tag-not15': ("{% if not foo and bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'no'), - - 'if-tag-not16': ("{% if foo or not bar %}yes{% else %}no{% endif %}", {}, 'yes'), - 'if-tag-not17': ("{% if foo or not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'yes'), - 'if-tag-not18': ("{% if foo or not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'yes'), - 'if-tag-not19': ("{% if foo or not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'no'), - 'if-tag-not20': ("{% if foo or not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'yes'), - - 'if-tag-not21': ("{% if not foo or bar %}yes{% else %}no{% endif %}", {}, 'yes'), - 'if-tag-not22': ("{% if not foo or bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'yes'), - 'if-tag-not23': ("{% if not foo or bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'no'), - 'if-tag-not24': ("{% if not foo or bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'yes'), - 'if-tag-not25': ("{% if not foo or bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'yes'), - - 'if-tag-not26': ("{% if not foo and not bar %}yes{% else %}no{% endif %}", {}, 'yes'), - 'if-tag-not27': ("{% if not foo and not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'no'), - 'if-tag-not28': ("{% if not foo and not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'no'), - 'if-tag-not29': ("{% if not foo and not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'no'), - 'if-tag-not30': ("{% if not foo and not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'yes'), - - 'if-tag-not31': ("{% if not foo or not bar %}yes{% else %}no{% endif %}", {}, 'yes'), - 'if-tag-not32': ("{% if not foo or not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'no'), - 'if-tag-not33': ("{% if not foo or not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'yes'), - 'if-tag-not34': ("{% if not foo or not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'yes'), - 'if-tag-not35': ("{% if not foo or not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'yes'), - - # AND and OR raises a TemplateSyntaxError - 'if-tag-error01': ("{% if foo or bar and baz %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, template.TemplateSyntaxError), - 'if-tag-error02': ("{% if foo and %}yes{% else %}no{% endif %}", {'foo': True}, template.TemplateSyntaxError), - 'if-tag-error03': ("{% if foo or %}yes{% else %}no{% endif %}", {'foo': True}, template.TemplateSyntaxError), - 'if-tag-error04': ("{% if not foo and %}yes{% else %}no{% endif %}", {'foo': True}, template.TemplateSyntaxError), - 'if-tag-error05': ("{% if not foo or %}yes{% else %}no{% endif %}", {'foo': True}, template.TemplateSyntaxError), - - ### IFCHANGED TAG ######################################################### - 'ifchanged01': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', { 'num': (1,2,3) }, '123'), - 'ifchanged02': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', { 'num': (1,1,3) }, '13'), - 'ifchanged03': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', { 'num': (1,1,1) }, '1'), - - ### IFEQUAL TAG ########################################################### - 'ifequal01': ("{% ifequal a b %}yes{% endifequal %}", {"a": 1, "b": 2}, ""), - 'ifequal02': ("{% ifequal a b %}yes{% endifequal %}", {"a": 1, "b": 1}, "yes"), - 'ifequal03': ("{% ifequal a b %}yes{% else %}no{% endifequal %}", {"a": 1, "b": 2}, "no"), - 'ifequal04': ("{% ifequal a b %}yes{% else %}no{% endifequal %}", {"a": 1, "b": 1}, "yes"), - 'ifequal05': ("{% ifequal a 'test' %}yes{% else %}no{% endifequal %}", {"a": "test"}, "yes"), - 'ifequal06': ("{% ifequal a 'test' %}yes{% else %}no{% endifequal %}", {"a": "no"}, "no"), - 'ifequal07': ('{% ifequal a "test" %}yes{% else %}no{% endifequal %}', {"a": "test"}, "yes"), - 'ifequal08': ('{% ifequal a "test" %}yes{% else %}no{% endifequal %}', {"a": "no"}, "no"), - 'ifequal09': ('{% ifequal a "test" %}yes{% else %}no{% endifequal %}', {}, "no"), - 'ifequal10': ('{% ifequal a b %}yes{% else %}no{% endifequal %}', {}, "yes"), - - # SMART SPLITTING - 'ifequal-split01': ('{% ifequal a "test man" %}yes{% else %}no{% endifequal %}', {}, "no"), - 'ifequal-split02': ('{% ifequal a "test man" %}yes{% else %}no{% endifequal %}', {'a': 'foo'}, "no"), - 'ifequal-split03': ('{% ifequal a "test man" %}yes{% else %}no{% endifequal %}', {'a': 'test man'}, "yes"), - 'ifequal-split04': ("{% ifequal a 'test man' %}yes{% else %}no{% endifequal %}", {'a': 'test man'}, "yes"), - 'ifequal-split05': ("{% ifequal a 'i \"love\" you' %}yes{% else %}no{% endifequal %}", {'a': ''}, "no"), - 'ifequal-split06': ("{% ifequal a 'i \"love\" you' %}yes{% else %}no{% endifequal %}", {'a': 'i "love" you'}, "yes"), - 'ifequal-split07': ("{% ifequal a 'i \"love\" you' %}yes{% else %}no{% endifequal %}", {'a': 'i love you'}, "no"), - 'ifequal-split08': (r"{% ifequal a 'I\'m happy' %}yes{% else %}no{% endifequal %}", {'a': "I'm happy"}, "yes"), - 'ifequal-split09': (r"{% ifequal a 'slash\man' %}yes{% else %}no{% endifequal %}", {'a': r"slash\man"}, "yes"), - 'ifequal-split10': (r"{% ifequal a 'slash\man' %}yes{% else %}no{% endifequal %}", {'a': r"slashman"}, "no"), - - ### IFNOTEQUAL TAG ######################################################## - 'ifnotequal01': ("{% ifnotequal a b %}yes{% endifnotequal %}", {"a": 1, "b": 2}, "yes"), - 'ifnotequal02': ("{% ifnotequal a b %}yes{% endifnotequal %}", {"a": 1, "b": 1}, ""), - 'ifnotequal03': ("{% ifnotequal a b %}yes{% else %}no{% endifnotequal %}", {"a": 1, "b": 2}, "yes"), - 'ifnotequal04': ("{% ifnotequal a b %}yes{% else %}no{% endifnotequal %}", {"a": 1, "b": 1}, "no"), - - ### INCLUDE TAG ########################################################### - 'include01': ('{% include "basic-syntax01" %}', {}, "something cool"), - 'include02': ('{% include "basic-syntax02" %}', {'headline': 'Included'}, "Included"), - 'include03': ('{% include template_name %}', {'template_name': 'basic-syntax02', 'headline': 'Included'}, "Included"), - 'include04': ('a{% include "nonexistent" %}b', {}, "ab"), - - ### INHERITANCE ########################################################### - - # Standard template with no inheritance - 'inheritance01': ("1{% block first %}_{% endblock %}3{% block second %}_{% endblock %}", {}, '1_3_'), - - # Standard two-level inheritance - 'inheritance02': ("{% extends 'inheritance01' %}{% block first %}2{% endblock %}{% block second %}4{% endblock %}", {}, '1234'), - - # Three-level with no redefinitions on third level - 'inheritance03': ("{% extends 'inheritance02' %}", {}, '1234'), - - # Two-level with no redefinitions on second level - 'inheritance04': ("{% extends 'inheritance01' %}", {}, '1_3_'), - - # Two-level with double quotes instead of single quotes - 'inheritance05': ('{% extends "inheritance02" %}', {}, '1234'), - - # Three-level with variable parent-template name - 'inheritance06': ("{% extends foo %}", {'foo': 'inheritance02'}, '1234'), - - # Two-level with one block defined, one block not defined - 'inheritance07': ("{% extends 'inheritance01' %}{% block second %}5{% endblock %}", {}, '1_35'), - - # Three-level with one block defined on this level, two blocks defined next level - 'inheritance08': ("{% extends 'inheritance02' %}{% block second %}5{% endblock %}", {}, '1235'), - - # Three-level with second and third levels blank - 'inheritance09': ("{% extends 'inheritance04' %}", {}, '1_3_'), - - # Three-level with space NOT in a block -- should be ignored - 'inheritance10': ("{% extends 'inheritance04' %} ", {}, '1_3_'), - - # Three-level with both blocks defined on this level, but none on second level - 'inheritance11': ("{% extends 'inheritance04' %}{% block first %}2{% endblock %}{% block second %}4{% endblock %}", {}, '1234'), - - # Three-level with this level providing one and second level providing the other - 'inheritance12': ("{% extends 'inheritance07' %}{% block first %}2{% endblock %}", {}, '1235'), - - # Three-level with this level overriding second level - 'inheritance13': ("{% extends 'inheritance02' %}{% block first %}a{% endblock %}{% block second %}b{% endblock %}", {}, '1a3b'), - - # A block defined only in a child template shouldn't be displayed - 'inheritance14': ("{% extends 'inheritance01' %}{% block newblock %}NO DISPLAY{% endblock %}", {}, '1_3_'), - - # A block within another block - 'inheritance15': ("{% extends 'inheritance01' %}{% block first %}2{% block inner %}inner{% endblock %}{% endblock %}", {}, '12inner3_'), - - # A block within another block (level 2) - 'inheritance16': ("{% extends 'inheritance15' %}{% block inner %}out{% endblock %}", {}, '12out3_'), - - # {% load %} tag (parent -- setup for exception04) - 'inheritance17': ("{% load testtags %}{% block first %}1234{% endblock %}", {}, '1234'), - - # {% load %} tag (standard usage, without inheritance) - 'inheritance18': ("{% load testtags %}{% echo this that theother %}5678", {}, 'this that theother5678'), - - # {% load %} tag (within a child template) - 'inheritance19': ("{% extends 'inheritance01' %}{% block first %}{% load testtags %}{% echo 400 %}5678{% endblock %}", {}, '140056783_'), - - # Two-level inheritance with {{ block.super }} - 'inheritance20': ("{% extends 'inheritance01' %}{% block first %}{{ block.super }}a{% endblock %}", {}, '1_a3_'), - - # Three-level inheritance with {{ block.super }} from parent - 'inheritance21': ("{% extends 'inheritance02' %}{% block first %}{{ block.super }}a{% endblock %}", {}, '12a34'), - - # Three-level inheritance with {{ block.super }} from grandparent - 'inheritance22': ("{% extends 'inheritance04' %}{% block first %}{{ block.super }}a{% endblock %}", {}, '1_a3_'), - - # Three-level inheritance with {{ block.super }} from parent and grandparent - 'inheritance23': ("{% extends 'inheritance20' %}{% block first %}{{ block.super }}b{% endblock %}", {}, '1_ab3_'), - - # Inheritance from local context without use of template loader - 'inheritance24': ("{% extends context_template %}{% block first %}2{% endblock %}{% block second %}4{% endblock %}", {'context_template': template.Template("1{% block first %}_{% endblock %}3{% block second %}_{% endblock %}")}, '1234'), - - # Inheritance from local context with variable parent template - 'inheritance25': ("{% extends context_template.1 %}{% block first %}2{% endblock %}{% block second %}4{% endblock %}", {'context_template': [template.Template("Wrong"), template.Template("1{% block first %}_{% endblock %}3{% block second %}_{% endblock %}")]}, '1234'), - - ### I18N ################################################################## - - # {% spaceless %} tag - 'spaceless01': ("{% spaceless %} text {% endspaceless %}", {}, " text "), - 'spaceless02': ("{% spaceless %} \n text \n {% endspaceless %}", {}, " text "), - 'spaceless03': ("{% spaceless %}text{% endspaceless %}", {}, "text"), - - # simple translation of a string delimited by ' - 'i18n01': ("{% load i18n %}{% trans 'xxxyyyxxx' %}", {}, "xxxyyyxxx"), - - # simple translation of a string delimited by " - 'i18n02': ('{% load i18n %}{% trans "xxxyyyxxx" %}', {}, "xxxyyyxxx"), - - # simple translation of a variable - 'i18n03': ('{% load i18n %}{% blocktrans %}{{ anton }}{% endblocktrans %}', {'anton': 'xxxyyyxxx'}, "xxxyyyxxx"), - - # simple translation of a variable and filter - 'i18n04': ('{% load i18n %}{% blocktrans with anton|lower as berta %}{{ berta }}{% endblocktrans %}', {'anton': 'XXXYYYXXX'}, "xxxyyyxxx"), - - # simple translation of a string with interpolation - 'i18n05': ('{% load i18n %}{% blocktrans %}xxx{{ anton }}xxx{% endblocktrans %}', {'anton': 'yyy'}, "xxxyyyxxx"), - - # simple translation of a string to german - 'i18n06': ('{% load i18n %}{% trans "Page not found" %}', {'LANGUAGE_CODE': 'de'}, "Seite nicht gefunden"), - - # translation of singular form - 'i18n07': ('{% load i18n %}{% blocktrans count number as counter %}singular{% plural %}plural{% endblocktrans %}', {'number': 1}, "singular"), - - # translation of plural form - 'i18n08': ('{% load i18n %}{% blocktrans count number as counter %}singular{% plural %}plural{% endblocktrans %}', {'number': 2}, "plural"), - - # simple non-translation (only marking) of a string to german - 'i18n09': ('{% load i18n %}{% trans "Page not found" noop %}', {'LANGUAGE_CODE': 'de'}, "Page not found"), - - # translation of a variable with a translated filter - 'i18n10': ('{{ bool|yesno:_("ja,nein") }}', {'bool': True}, 'ja'), - - # translation of a variable with a non-translated filter - 'i18n11': ('{{ bool|yesno:"ja,nein" }}', {'bool': True}, 'ja'), - - # usage of the get_available_languages tag - 'i18n12': ('{% load i18n %}{% get_available_languages as langs %}{% for lang in langs %}{% ifequal lang.0 "de" %}{{ lang.0 }}{% endifequal %}{% endfor %}', {}, 'de'), - - # translation of a constant string - 'i18n13': ('{{ _("Page not found") }}', {'LANGUAGE_CODE': 'de'}, 'Seite nicht gefunden'), - - ### MULTILINE ############################################################# - - 'multiline01': (""" - Hello, - boys. - How - are - you - gentlemen. - """, - {}, - """ - Hello, - boys. - How - are - you - gentlemen. - """), - - ### REGROUP TAG ########################################################### - 'regroup01': ('{% regroup data by bar as grouped %}' + \ - '{% for group in grouped %}' + \ - '{{ group.grouper }}:' + \ - '{% for item in group.list %}' + \ - '{{ item.foo }}' + \ - '{% endfor %},' + \ - '{% endfor %}', - {'data': [ {'foo':'c', 'bar':1}, - {'foo':'d', 'bar':1}, - {'foo':'a', 'bar':2}, - {'foo':'b', 'bar':2}, - {'foo':'x', 'bar':3} ]}, - '1:cd,2:ab,3:x,'), - - # Test for silent failure when target variable isn't found - 'regroup02': ('{% regroup data by bar as grouped %}' + \ - '{% for group in grouped %}' + \ - '{{ group.grouper }}:' + \ - '{% for item in group.list %}' + \ - '{{ item.foo }}' + \ - '{% endfor %},' + \ - '{% endfor %}', - {}, 'INVALID:INVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALID,'), - - ### TEMPLATETAG TAG ####################################################### - 'templatetag01': ('{% templatetag openblock %}', {}, '{%'), - 'templatetag02': ('{% templatetag closeblock %}', {}, '%}'), - 'templatetag03': ('{% templatetag openvariable %}', {}, '{{'), - 'templatetag04': ('{% templatetag closevariable %}', {}, '}}'), - 'templatetag05': ('{% templatetag %}', {}, template.TemplateSyntaxError), - 'templatetag06': ('{% templatetag foo %}', {}, template.TemplateSyntaxError), - 'templatetag07': ('{% templatetag openbrace %}', {}, '{'), - 'templatetag08': ('{% templatetag closebrace %}', {}, '}'), - 'templatetag09': ('{% templatetag openbrace %}{% templatetag openbrace %}', {}, '{{'), - 'templatetag10': ('{% templatetag closebrace %}{% templatetag closebrace %}', {}, '}}'), - - ### WIDTHRATIO TAG ######################################################## - 'widthratio01': ('{% widthratio a b 0 %}', {'a':50,'b':100}, '0'), - 'widthratio02': ('{% widthratio a b 100 %}', {'a':0,'b':0}, ''), - 'widthratio03': ('{% widthratio a b 100 %}', {'a':0,'b':100}, '0'), - 'widthratio04': ('{% widthratio a b 100 %}', {'a':50,'b':100}, '50'), - 'widthratio05': ('{% widthratio a b 100 %}', {'a':100,'b':100}, '100'), - - # 62.5 should round to 63 - 'widthratio06': ('{% widthratio a b 100 %}', {'a':50,'b':80}, '63'), - - # 71.4 should round to 71 - 'widthratio07': ('{% widthratio a b 100 %}', {'a':50,'b':70}, '71'), - - # Raise exception if we don't have 3 args, last one an integer - 'widthratio08': ('{% widthratio %}', {}, template.TemplateSyntaxError), - 'widthratio09': ('{% widthratio a b %}', {'a':50,'b':100}, template.TemplateSyntaxError), - 'widthratio10': ('{% widthratio a b 100.0 %}', {'a':50,'b':100}, template.TemplateSyntaxError), - - ### NOW TAG ######################################################## - # Simple case - 'now01' : ('{% now "j n Y"%}', {}, str(datetime.now().day) + ' ' + str(datetime.now().month) + ' ' + str(datetime.now().year)), - - # Check parsing of escaped and special characters - 'now02' : ('{% now "j "n" Y"%}', {}, template.TemplateSyntaxError), -# 'now03' : ('{% now "j \"n\" Y"%}', {}, str(datetime.now().day) + '"' + str(datetime.now().month) + '"' + str(datetime.now().year)), -# 'now04' : ('{% now "j \nn\n Y"%}', {}, str(datetime.now().day) + '\n' + str(datetime.now().month) + '\n' + str(datetime.now().year)) - - ### TIMESINCE TAG ################################################## - # Default compare with datetime.now() - 'timesince01' : ('{{ a|timesince }}', {'a':datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1 minute'), - 'timesince02' : ('{{ a|timesince }}', {'a':(datetime.now() - timedelta(days=1, minutes = 1))}, '1 day'), - 'timesince03' : ('{{ a|timesince }}', {'a':(datetime.now() - - timedelta(hours=1, minutes=25, seconds = 10))}, '1 hour, 25 minutes'), - - # Compare to a given parameter - 'timesince04' : ('{{ a|timesince:b }}', {'a':NOW + timedelta(days=2), 'b':NOW + timedelta(days=1)}, '1 day'), - 'timesince05' : ('{{ a|timesince:b }}', {'a':NOW + timedelta(days=2, minutes=1), 'b':NOW + timedelta(days=2)}, '1 minute'), - - # Check that timezone is respected - 'timesince06' : ('{{ a|timesince:b }}', {'a':NOW_tz + timedelta(hours=8), 'b':NOW_tz}, '8 hours'), - - ### TIMEUNTIL TAG ################################################## - # Default compare with datetime.now() - 'timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2 minutes'), - 'timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1 day'), - 'timeuntil03' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(hours=8, minutes=10, seconds = 10))}, '8 hours, 10 minutes'), - - # Compare to a given parameter - 'timeuntil04' : ('{{ a|timeuntil:b }}', {'a':NOW - timedelta(days=1), 'b':NOW - timedelta(days=2)}, '1 day'), - 'timeuntil05' : ('{{ a|timeuntil:b }}', {'a':NOW - timedelta(days=2), 'b':NOW - timedelta(days=2, minutes=1)}, '1 minute'), -} - -def test_template_loader(template_name, template_dirs=None): - "A custom template loader that loads the unit-test templates." - try: - return (TEMPLATE_TESTS[template_name][0] , "test:%s" % template_name) - except KeyError: - raise template.TemplateDoesNotExist, template_name - -def run_tests(verbosity=0, standalone=False): - # Register our custom template loader. - old_template_loaders = loader.template_source_loaders - loader.template_source_loaders = [test_template_loader] - - failed_tests = [] - tests = TEMPLATE_TESTS.items() - tests.sort() - - # Turn TEMPLATE_DEBUG off, because tests assume that. - old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False - # Set TEMPLATE_STRING_IF_INVALID to a known string - old_invalid, settings.TEMPLATE_STRING_IF_INVALID = settings.TEMPLATE_STRING_IF_INVALID, 'INVALID' - - for name, vals in tests: - install() - if 'LANGUAGE_CODE' in vals[1]: - activate(vals[1]['LANGUAGE_CODE']) - else: - activate('en-us') - try: - output = loader.get_template(name).render(template.Context(vals[1])) - except Exception, e: - if e.__class__ == vals[2]: - if verbosity: - print "Template test: %s -- Passed" % name - else: - if verbosity: - traceback.print_exc() - print "Template test: %s -- FAILED. Got %s, exception: %s" % (name, e.__class__, e) - failed_tests.append(name) - continue - if 'LANGUAGE_CODE' in vals[1]: - deactivate() - if output == vals[2]: - if verbosity: - print "Template test: %s -- Passed" % name - else: - if verbosity: - print "Template test: %s -- FAILED. Expected %r, got %r" % (name, vals[2], output) - failed_tests.append(name) - loader.template_source_loaders = old_template_loaders - deactivate() - settings.TEMPLATE_DEBUG = old_td - settings.TEMPLATE_STRING_IF_INVALID = old_invalid - - if failed_tests and not standalone: - msg = "Template tests %s failed." % failed_tests - if not verbosity: - msg += " Re-run tests with -v1 to see actual failures" - raise Exception, msg - -if __name__ == "__main__": - run_tests(1, True) diff --git a/tests/othertests/thread_isolation.py b/tests/othertests/thread_isolation.py deleted file mode 100644 index f1d1f9129b..0000000000 --- a/tests/othertests/thread_isolation.py +++ /dev/null @@ -1,300 +0,0 @@ -# tests that db settings can be different in different threads -# -# -# What's going on here: -# -# Simulating multiple web requests in a threaded environment, one in -# which settings are different for each request. So we replace -# django.conf.settings with a thread local, with different -# configurations in each thread, and then fire off three -# simultaneous requests (using a condition to sync them up), and -# test that each thread sees its own settings and the models in each -# thread attempt to connect to the correct database as per their -# settings. -# - - -import copy -import os -import sys -import threading -from thread import get_ident - -from django.conf import settings, UserSettingsHolder -from django.core.handlers.wsgi import WSGIHandler -from django.db import models, model_connection_name, _default, connection, \ - connections -from django.http import HttpResponse - -try: - # Only exists in Python 2.4+ - from threading import local -except ImportError: - # Import copy of _thread_local.py from Python 2.4 - from django.utils._threading_local import local - -# state holder -S = {} - -# models -class MQ(models.Model): - val = models.CharField(maxlength=10) - class Meta: - app_label = 'ti' - - -class MX(models.Model): - val = models.CharField(maxlength=10) - class Meta: - app_label = 'ti' - - -class MY(models.Model): - val = models.CharField(maxlength=10) - class Meta: - app_label = 'ti' - -# eventused to synchronize threads so we can be sure they are running -# together -ev = threading.Event() - - -def test_one(path, request): - """Start out with settings as originally configured""" - from django.conf import settings - debug("test_one: %s", settings.OTHER_DATABASES) - - assert model_connection_name(MQ) == _default - assert model_connection_name(MX) == 'django_test_db_a', \ - "%s != 'django_test_db_a'" % model_connection_name(MX) - assert MX._default_manager.db.connection.settings.DATABASE_NAME == \ - settings.OTHER_DATABASES['django_test_db_a']['DATABASE_NAME'] - assert model_connection_name(MY) == 'django_test_db_b' - assert MY._default_manager.db.connection.settings.DATABASE_NAME == \ - settings.OTHER_DATABASES['django_test_db_b']['DATABASE_NAME'], \ - "%s != %s" % \ - (MY._default_manager.db.connection.settings.DATABASE_NAME, - settings.OTHER_DATABASES['django_test_db_b']['DATABASE_NAME']) - assert MQ._default_manager.db.connection is \ - connections[_default].connection - assert MQ._default_manager.db.connection.settings.DATABASE_NAME == \ - settings.DATABASE_NAME - assert connection.settings.DATABASE_NAME == settings.DATABASE_NAME - - -def test_two(path, request): - """Between the first and second requests, settings change to assign - model MY to a different connection - """ - # from django.conf import settings - debug("test_two: %s", settings.OTHER_DATABASES) - - try: - assert model_connection_name(MQ) == _default - assert model_connection_name(MX) == 'django_test_db_a' - assert MX._default_manager.db.connection.settings.DATABASE_NAME == \ - settings.OTHER_DATABASES['django_test_db_a']['DATABASE_NAME'] - assert model_connection_name(MY) == _default - assert MY._default_manager.db.connection.settings.DATABASE_NAME == \ - settings.DATABASE_NAME, "%s != %s" % \ - (MY._default_manager.db.connection.settings.DATABASE_NAME, - settings.DATABASE_NAME) - assert MQ._default_manager.db.connection is \ - connections[_default].connection - assert MQ._default_manager.db.connection.settings.DATABASE_NAME == \ - settings.DATABASE_NAME - assert connection.settings.DATABASE_NAME == settings.DATABASE_NAME - except: - S.setdefault('errors',[]).append(sys.exc_info()) - - -def test_three(path, request): - """Between the 2nd and 3rd requests, the settings at the names in - OTHER_DATABASES have changed. - """ - # from django.conf import settings - debug("3 %s: %s", get_ident(), settings.OTHER_DATABASES) - debug("3 %s: default: %s", get_ident(), settings.DATABASE_NAME) - debug("3 %s: conn: %s", get_ident(), connection.settings.DATABASE_NAME) - - try: - assert model_connection_name(MQ) == _default - assert model_connection_name(MX) == 'django_test_db_b' - assert MX._default_manager.db.connection.settings.DATABASE_NAME == \ - settings.OTHER_DATABASES['django_test_db_b']['DATABASE_NAME'],\ - "%s != %s" % \ - (MX._default_manager.db.connection.settings.DATABASE_NAME, - settings.OTHER_DATABASES['django_test_db_b']['DATABASE_NAME']) - assert model_connection_name(MY) == 'django_test_db_a' - assert MY._default_manager.db.connection.settings.DATABASE_NAME == \ - settings.OTHER_DATABASES['django_test_db_a']['DATABASE_NAME'],\ - "%s != %s" % \ - (MY._default_manager.db.connection.settings.DATABASE_NAME, - settings.OTHER_DATABASES['django_test_db_a']['DATABASE_NAME']) - assert MQ._default_manager.db.connection is \ - connections[_default].connection - assert connection.settings.DATABASE_NAME == \ - settings.OTHER_DATABASES['django_test_db_a']['DATABASE_NAME'],\ - "%s != %s" % \ - (connection.settings.DATABASE_NAME, - settings.OTHER_DATABASES['django_test_db_a']['DATABASE_NAME']) - except: - S.setdefault('errors',[]).append(sys.exc_info()) - - - -# helpers -def thread_two(func, *arg): - def start(): - # from django.conf import settings - settings.OTHER_DATABASES['django_test_db_b']['MODELS'] = [] - - debug("t2 ODB: %s", settings.OTHER_DATABASES) - debug("t2 waiting") - ev.wait(2.0) - func(*arg) - debug("t2 complete") - t2 = threading.Thread(target=start) - t2.start() - return t2 - - -def thread_three(func, *arg): - def start(): - # from django.conf import settings - settings.OTHER_DATABASES['django_test_db_b']['MODELS'] = ['ti.MY'] - settings.OTHER_DATABASES['django_test_db_b'], \ - settings.OTHER_DATABASES['django_test_db_a'] = \ - settings.OTHER_DATABASES['django_test_db_a'], \ - settings.OTHER_DATABASES['django_test_db_b'] - - settings.DATABASE_NAME = \ - settings.OTHER_DATABASES['django_test_db_a']['DATABASE_NAME'] - - debug("t3 ODB: %s", settings.OTHER_DATABASES) - debug("3 %s: start: default: %s", get_ident(), settings.DATABASE_NAME) - debug("3 %s: start: conn: %s", get_ident(), - connection.settings.DATABASE_NAME) - - debug("t3 waiting") - ev.wait(2.0) - func(*arg) - debug("t3 complete") - t3 = threading.Thread(target=start) - t3.start() - return t3 - -# helpers -class LocalSettings: - """Settings holder that allows thread-local overrides of defaults. - """ - def __init__(self, defaults): - self._defaults = defaults - self._local = local() - - def __getattr__(self, attr): - if attr in ('_defaults', '_local'): - return self.__dict__[attr] - _local = self.__dict__['_local'] - _defaults = self.__dict__['_defaults'] - debug("LS get %s (%s)", attr, hasattr(_local, attr)) - if not hasattr(_local, attr): - # Make sure everything we return is the local version; this - # avoids sets to deep datastructures overwriting the defaults - setattr(_local, attr, copy.deepcopy(getattr(_defaults, attr))) - return getattr(_local, attr) - - def __setattr__(self, attr, val): - if attr in ('_defaults', '_local'): - self.__dict__[attr] = val - else: - debug("LS set local %s = %s", attr, val) - setattr(self.__dict__['_local'], attr, val) - -class MockHandler(WSGIHandler): - - def __init__(self, test): - self.test = test - super(MockHandler, self).__init__() - - def get_response(self, path, request): - # debug("mock handler answering %s, %s", path, request) - return HttpResponse(self.test(path, request)) - - -def pr(*arg): - if S['verbosity'] >= 1: - msg, arg = arg[0], arg[1:] - print msg % arg - - -def debug(*arg): - if S['verbosity'] >= 2: - msg, arg = arg[0], arg[1:] - print msg % arg - - -def setup(): - debug("setup") - S['settings'] = settings._target - settings._target = UserSettingsHolder(copy.deepcopy(settings._target)) - settings.OTHER_DATABASES['django_test_db_a']['MODELS'] = ['ti.MX'] - settings.OTHER_DATABASES['django_test_db_b']['MODELS'] = ['ti.MY'] - - # normal settings holders aren't thread-safe, so we need to substitute - # one that is (and so allows per-thread settings) - holder = settings._target - settings._target = LocalSettings(holder) - - -def teardown(): - debug("teardown") - settings._target = S['settings'] - - -def start_response(code, headers): - debug("start response: %s %s", code, headers) - pass - - -def main(): - debug("running tests") - - env = os.environ.copy() - env['PATH_INFO'] = '/' - env['REQUEST_METHOD'] = 'GET' - - t2 = thread_two(MockHandler(test_two), env, start_response) - t3 = thread_three(MockHandler(test_three), env, start_response) - - try: - ev.set() - MockHandler(test_one)(env, start_response) - finally: - t2.join() - t3.join() - err = S.get('errors', []) - if err: - import traceback - for e in err: - traceback.print_exception(*e) - raise AssertionError("%s thread%s failed" % - (len(err), len(err) > 1 and 's' or '')) - -def run_tests(verbosity=0): - S['verbosity'] = verbosity - - setup() - try: - main() - finally: - teardown() - - -if __name__ == '__main__': - from django.conf import settings - - settings.DATABASE_NAME = ':memory:' - print "MAIN start! ", connection.settings.DATABASE_NAME - connection.cursor() - run_tests(2) diff --git a/tests/othertests/__init__.py b/tests/regressiontests/ansi_sql/__init__.py similarity index 100% rename from tests/othertests/__init__.py rename to tests/regressiontests/ansi_sql/__init__.py diff --git a/tests/othertests/ansi_sql.py b/tests/regressiontests/ansi_sql/models.py similarity index 73% rename from tests/othertests/ansi_sql.py rename to tests/regressiontests/ansi_sql/models.py index 3242172ec3..625106f439 100644 --- a/tests/othertests/ansi_sql.py +++ b/tests/regressiontests/ansi_sql/models.py @@ -10,7 +10,7 @@ >>> builder.get_create_table(Car) ([BoundStatement('CREATE TABLE "ansi_sql_car" (...);')], {}) >>> builder.models_already_seen -Set([]) +Set([]) >>> builder.models_already_seen = set() # test that styles are used @@ -20,7 +20,7 @@ Set([]) # test pending relationships >>> builder.models_already_seen = set() >>> builder.get_create_table(Mod) -([BoundStatement('CREATE TABLE "ansi_sql_mod" (..."car_id" integer NOT NULL,...);')], {: [BoundStatement('ALTER TABLE "ansi_sql_mod" ADD CONSTRAINT ... FOREIGN KEY ("car_id") REFERENCES "ansi_sql_car" ("id");')]}) +([BoundStatement('CREATE TABLE "ansi_sql_mod" (..."car_id" integer NOT NULL,...);')], {: [BoundStatement('ALTER TABLE "ansi_sql_mod" ADD CONSTRAINT ... FOREIGN KEY ("car_id") REFERENCES "ansi_sql_car" ("id");')]}) >>> builder.models_already_seen = set() >>> builder.get_create_table(Car) ([BoundStatement('CREATE TABLE "ansi_sql_car" (...);')], {}) @@ -31,7 +31,7 @@ Set([]) >>> builder.get_create_table(Collector) ([BoundStatement('CREATE TABLE "ansi_sql_collector" (...);')], {}) >>> builder.get_create_many_to_many(Collector) -{: [BoundStatement('CREATE TABLE "ansi_sql_collector_cars" (...);')]} +{: [BoundStatement('CREATE TABLE "ansi_sql_collector_cars" (...);')]} # test indexes >>> builder.get_create_indexes(Car) @@ -43,7 +43,7 @@ Set([]) # test initial data # patch builder so that it looks for initial data where we want it to ->>> builder.get_initialdata_path = othertests_sql +# >>> builder.get_initialdata_path = othertests_sql >>> builder.get_initialdata(Car) [BoundStatement('insert into ansi_sql_car (...)...values (...);')] @@ -78,12 +78,12 @@ True >>> Mod._default_manager.db.backend.supports_constraints = real_cnst """ -import os -import sys -from django.conf import settings -from django.core.management import install +#import os +#import sys +#from django.conf import settings +#from django.core.management import install from django.db import models -from django.db.models import loading +#from django.db.models import loading # For Python 2.3 @@ -97,26 +97,17 @@ class Car(models.Model): model = models.CharField(maxlength=32) year = models.IntegerField() condition = models.CharField(maxlength=32) - - class Meta: - app_label = 'ansi_sql' class Collector(models.Model): name = models.CharField(maxlength=32) cars = models.ManyToManyField(Car) - - class Meta: - app_label = 'ansi_sql' class Mod(models.Model): car = models.ForeignKey(Car) part = models.CharField(maxlength=32, db_index=True) description = models.TextField() - - class Meta: - app_label = 'ansi_sql' class mockstyle: @@ -127,21 +118,21 @@ class mockstyle: return lambda text: "%s(%s)" % (attr, text) -def othertests_sql(mod): - """Look in othertests/sql for sql initialdata""" - return os.path.normpath(os.path.join(os.path.dirname(__file__), 'sql')) +# def othertests_sql(mod): +# """Look in othertests/sql for sql initialdata""" +# return os.path.normpath(os.path.join(os.path.dirname(__file__), 'sql')) -# install my models and force myself into the registry of apps and models -# (without this, references and such to/from the models installed here -# won't be picked up in get_models(), since get_models() looks only at -# models from apps in INSTALLED_APPS, and the model and app lookup rules -# enforce a module structure that this test file can't follow) -Car.objects.install() -Collector.objects.install() -Mod.objects.install() -loading.get_apps() -loading._app_list.append(sys.modules[__name__]) +# # install my models and force myself into the registry of apps and models +# # (without this, references and such to/from the models installed here +# # won't be picked up in get_models(), since get_models() looks only at +# # models from apps in INSTALLED_APPS, and the model and app lookup rules +# # enforce a module structure that this test file can't follow) +# Car.objects.install() +# Collector.objects.install() +# Mod.objects.install() +# loading.get_apps() +# loading._app_list.append(sys.modules[__name__]) -# model lookup only works for pk.models, so fake up my module name -__name__ = 'othertests.ansi_sql.models' +# # model lookup only works for pk.models, so fake up my module name +# __name__ = 'othertests.ansi_sql.models' diff --git a/tests/othertests/sql/car.sql b/tests/regressiontests/ansi_sql/sql/car.sql similarity index 100% rename from tests/othertests/sql/car.sql rename to tests/regressiontests/ansi_sql/sql/car.sql diff --git a/tests/regressiontests/cache/__init__.py b/tests/regressiontests/cache/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/cache/models.py b/tests/regressiontests/cache/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py new file mode 100644 index 0000000000..cf58ab321a --- /dev/null +++ b/tests/regressiontests/cache/tests.py @@ -0,0 +1,71 @@ +# Unit tests for cache framework +# Uses whatever cache backend is set in the test settings file. + +from django.core.cache import cache +import time, unittest + +# functions/classes for complex data type tests +def f(): + return 42 +class C: + def m(n): + return 24 + +class Cache(unittest.TestCase): + def test_simple(self): + # simple set/get + cache.set("key", "value") + self.assertEqual(cache.get("key"), "value") + + def test_non_existent(self): + # get with non-existent keys + self.assertEqual(cache.get("does not exist"), None) + self.assertEqual(cache.get("does not exist", "bang!"), "bang!") + + def test_get_many(self): + # get_many + cache.set('a', 'a') + cache.set('b', 'b') + cache.set('c', 'c') + cache.set('d', 'd') + self.assertEqual(cache.get_many(['a', 'c', 'd']), {'a' : 'a', 'c' : 'c', 'd' : 'd'}) + self.assertEqual(cache.get_many(['a', 'b', 'e']), {'a' : 'a', 'b' : 'b'}) + + def test_delete(self): + # delete + cache.set("key1", "spam") + cache.set("key2", "eggs") + self.assertEqual(cache.get("key1"), "spam") + cache.delete("key1") + self.assertEqual(cache.get("key1"), None) + self.assertEqual(cache.get("key2"), "eggs") + + def test_has_key(self): + # has_key + cache.set("hello", "goodbye") + self.assertEqual(cache.has_key("hello"), True) + self.assertEqual(cache.has_key("goodbye"), False) + + def test_data_types(self): + # test data types + stuff = { + 'string' : 'this is a string', + 'int' : 42, + 'list' : [1, 2, 3, 4], + 'tuple' : (1, 2, 3, 4), + 'dict' : {'A': 1, 'B' : 2}, + 'function' : f, + 'class' : C, + } + for (key, value) in stuff.items(): + cache.set(key, value) + self.assertEqual(cache.get(key), value) + + def test_expiration(self): + # expiration + cache.set('expire', 'very quickly', 1) + time.sleep(2) + self.assertEqual(cache.get("expire"), None) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/regressiontests/dateformat/__init__.py b/tests/regressiontests/dateformat/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/dateformat/models.py b/tests/regressiontests/dateformat/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/othertests/dateformat.py b/tests/regressiontests/dateformat/tests.py similarity index 100% rename from tests/othertests/dateformat.py rename to tests/regressiontests/dateformat/tests.py diff --git a/tests/regressiontests/db_typecasts/__init__.py b/tests/regressiontests/db_typecasts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/db_typecasts/models.py b/tests/regressiontests/db_typecasts/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/othertests/db_typecasts.py b/tests/regressiontests/db_typecasts/tests.py similarity index 81% rename from tests/othertests/db_typecasts.py rename to tests/regressiontests/db_typecasts/tests.py index ffc9b34aec..f4b77fe3a6 100644 --- a/tests/othertests/db_typecasts.py +++ b/tests/regressiontests/db_typecasts/tests.py @@ -1,7 +1,7 @@ # Unit tests for typecast functions in django.db.backends.util from django.db.backends import util as typecasts -import datetime +import datetime, unittest TEST_CASES = { 'typecast_date': ( @@ -45,7 +45,12 @@ TEST_CASES = { ), } -for k, v in TEST_CASES.items(): - for inpt, expected in v: - got = getattr(typecasts, k)(inpt) - assert got == expected, "In %s: %r doesn't match %r. Got %r instead." % (k, inpt, expected, got) +class DBTypeCasts(unittest.TestCase): + def test_typeCasts(self): + for k, v in TEST_CASES.items(): + for inpt, expected in v: + got = getattr(typecasts, k)(inpt) + assert got == expected, "In %s: %r doesn't match %r. Got %r instead." % (k, inpt, expected, got) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/regressiontests/defaultfilters/__init__.py b/tests/regressiontests/defaultfilters/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/defaultfilters/models.py b/tests/regressiontests/defaultfilters/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/othertests/defaultfilters.py b/tests/regressiontests/defaultfilters/tests.py similarity index 100% rename from tests/othertests/defaultfilters.py rename to tests/regressiontests/defaultfilters/tests.py diff --git a/tests/regressiontests/httpwrappers/__init__.py b/tests/regressiontests/httpwrappers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/httpwrappers/models.py b/tests/regressiontests/httpwrappers/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/othertests/httpwrappers.py b/tests/regressiontests/httpwrappers/tests.py similarity index 100% rename from tests/othertests/httpwrappers.py rename to tests/regressiontests/httpwrappers/tests.py diff --git a/tests/regressiontests/initial_sql_regress/models.py b/tests/regressiontests/initial_sql_regress/models.py index c4cf12bdf7..dedbba8e5c 100644 --- a/tests/regressiontests/initial_sql_regress/models.py +++ b/tests/regressiontests/initial_sql_regress/models.py @@ -7,7 +7,7 @@ from django.db import models class Simple(models.Model): name = models.CharField(maxlength = 50) -API_TESTS = "" +__test__ = {'API_TESTS':""} # NOTE: The format of the included SQL file for this test suite is important. # It must end with a trailing newline in order to test the fix for #2161. diff --git a/tests/regressiontests/manager_db/__init__.py b/tests/regressiontests/manager_db/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/manager_db/models.py b/tests/regressiontests/manager_db/models.py new file mode 100644 index 0000000000..8f1fb1319a --- /dev/null +++ b/tests/regressiontests/manager_db/models.py @@ -0,0 +1,5 @@ +from django.db import models + +class Insect(models.Model): + common_name = models.CharField(maxlength=64) + latin_name = models.CharField(maxlength=128) diff --git a/tests/regressiontests/manager_db/tests.py b/tests/regressiontests/manager_db/tests.py new file mode 100644 index 0000000000..d159553b25 --- /dev/null +++ b/tests/regressiontests/manager_db/tests.py @@ -0,0 +1,17 @@ +import unittest +from regressiontests.manager_db.models import Insect + +class TestManagerDBAccess(unittest.TestCase): + + def test_db_property(self): + m = Insect.objects + db = Insect.objects.db + assert db + assert db.connection + assert db.connection.cursor + assert db.backend + assert db.backend.quote_name + assert db.get_creation_module + +if __name__ == '__main__': + unittest.main() diff --git a/tests/regressiontests/manager_schema_manipulation/__init__.py b/tests/regressiontests/manager_schema_manipulation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/manager_schema_manipulation/models.py b/tests/regressiontests/manager_schema_manipulation/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/othertests/manager_schema_manipulation.py b/tests/regressiontests/manager_schema_manipulation/tests.py similarity index 89% rename from tests/othertests/manager_schema_manipulation.py rename to tests/regressiontests/manager_schema_manipulation/tests.py index 259b078d27..56befae7ce 100644 --- a/tests/othertests/manager_schema_manipulation.py +++ b/tests/regressiontests/manager_schema_manipulation/tests.py @@ -8,8 +8,8 @@ # save copy of settings so we can restore it later >>> odb = copy.deepcopy(settings.OTHER_DATABASES) ->>> settings.OTHER_DATABASES['django_test_db_a']['MODELS'] = [ 'msm.PA', 'msm.P', 'msm.PC' ] ->>> settings.OTHER_DATABASES['django_test_db_b']['MODELS'] = [ 'msm.QA', 'msm.QB', 'msm.QC', 'msm.QD' ] +>>> settings.OTHER_DATABASES['django_msm_test_db_a'] = { 'MODELS': ['msm.PA', 'msm.P', 'msm.PC']} +>>> settings.OTHER_DATABASES['django_msm_test_db_b'] = {'MODELS': ['msm.QA', 'msm.QB', 'msm.QC', 'msm.QD']} # default connection >>> class DA(models.Model): @@ -18,7 +18,7 @@ ... def __str__(self): ... return self.name -# connection django_test_db_a +# connection a >>> class PA(models.Model): ... name = models.CharField(maxlength=20) ... # This creates a cycle in the dependency graph @@ -50,7 +50,7 @@ ... class Meta: ... app_label = 'msm' -# connection django_test_db_b +# connection b >>> class QA(models.Model): ... name = models.CharField(maxlength=20) ... @@ -123,7 +123,7 @@ >>> PA._default_manager.db.backend.supports_constraints = True >>> result = PA.objects.install() >>> result -{: [BoundStatement('ALTER TABLE "msm_pa" ADD CONSTRAINT "c_id_referencing_msm_pc_id" FOREIGN KEY ("c_id") REFERENCES "msm_pc" ("id");')]} +{: [BoundStatement('ALTER TABLE "msm_pa" ADD CONSTRAINT "id_refs_c_id..." FOREIGN KEY ("c_id") REFERENCES "msm_pc" ("id");')]} # NOTE: restore real constraint flag >>> PA._default_manager.db.backend.supports_constraints = real_cnst diff --git a/tests/regressiontests/many_to_one_regress/models.py b/tests/regressiontests/many_to_one_regress/models.py index 485e928777..6c067446b1 100644 --- a/tests/regressiontests/many_to_one_regress/models.py +++ b/tests/regressiontests/many_to_one_regress/models.py @@ -10,4 +10,4 @@ class Second(models.Model): # created (the field names being lower-cased versions of their opposite # classes is important here). -API_TESTS = "" +__test__ = {'API_TESTS':""} diff --git a/tests/regressiontests/markup/__init__.py b/tests/regressiontests/markup/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/markup/models.py b/tests/regressiontests/markup/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/markup/tests.py b/tests/regressiontests/markup/tests.py new file mode 100644 index 0000000000..bd3f52b9dd --- /dev/null +++ b/tests/regressiontests/markup/tests.py @@ -0,0 +1,69 @@ +# Quick tests for the markup templatetags (django.contrib.markup) + +from django.template import Template, Context, add_to_builtins +import re +import unittest + +add_to_builtins('django.contrib.markup.templatetags.markup') + +class Templates(unittest.TestCase): + def test_textile(self): + try: + import textile + except ImportError: + textile = None + + textile_content = """Paragraph 1 + +Paragraph 2 with "quotes" and @code@""" + + t = Template("{{ textile_content|textile }}") + rendered = t.render(Context(locals())).strip() + if textile: + self.assertEqual(rendered, """

Paragraph 1

+ +

Paragraph 2 with “quotes” and code

""") + else: + self.assertEqual(rendered, textile_content) + + def test_markdown(self): + try: + import markdown + except ImportError: + markdown = None + + markdown_content = """Paragraph 1 + +## An h2""" + + t = Template("{{ markdown_content|markdown }}") + rendered = t.render(Context(locals())).strip() + if markdown: + pattern = re.compile("""

Paragraph 1\s*

\s*

\s*An h2

""") + self.assert_(pattern.match(rendered)) + else: + self.assertEqual(rendered, markdown_content) + + def test_docutils(self): + try: + import docutils + except ImportError: + docutils = None + + rest_content = """Paragraph 1 + +Paragraph 2 with a link_ + +.. _link: http://www.example.com/""" + + t = Template("{{ rest_content|restructuredtext }}") + rendered = t.render(Context(locals())).strip() + if docutils: + self.assertEqual(rendered, """

Paragraph 1

+

Paragraph 2 with a link

""") + else: + self.assertEqual(rendered, rest_content) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/regressiontests/one_to_one_regress/models.py b/tests/regressiontests/one_to_one_regress/models.py index 6cc1df4e5c..b81f4266e1 100644 --- a/tests/regressiontests/one_to_one_regress/models.py +++ b/tests/regressiontests/one_to_one_regress/models.py @@ -22,7 +22,7 @@ class Favorites(models.Model): def __str__(self): return "Favorites for %s" % self.name -API_TESTS = """ +__test__ = {'API_TESTS':""" # Regression test for #1064 and #1506: Check that we create models via the m2m # relation if the remote model has a OneToOneField. >>> p1 = Place(name='Demon Dogs', address='944 W. Fullerton') @@ -34,4 +34,4 @@ API_TESTS = """ >>> f.restaurants = [r] >>> f.restaurants.all() [] -""" +"""} diff --git a/tests/regressiontests/request_isolation/__init__.py b/tests/regressiontests/request_isolation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/request_isolation/models.py b/tests/regressiontests/request_isolation/models.py new file mode 100644 index 0000000000..aa33c07d77 --- /dev/null +++ b/tests/regressiontests/request_isolation/models.py @@ -0,0 +1,13 @@ +from django.db import models + +# models +class MX(models.Model): + val = models.CharField(maxlength=10) + class Meta: + app_label = 'ri' + + +class MY(models.Model): + val = models.CharField(maxlength=10) + class Meta: + app_label = 'ri' diff --git a/tests/regressiontests/request_isolation/tests.py b/tests/regressiontests/request_isolation/tests.py new file mode 100644 index 0000000000..b1c42d40bf --- /dev/null +++ b/tests/regressiontests/request_isolation/tests.py @@ -0,0 +1,100 @@ +# tests that db settings can change between requests +import copy +import os +import unittest +from django.conf import settings, UserSettingsHolder +from django.core.handlers.wsgi import WSGIHandler +from django.db import models, model_connection_name, _default, connection +from django.http import HttpResponse +from regressiontests.request_isolation.models import * + + +# helpers +class MockHandler(WSGIHandler): + + def __init__(self, test): + self.test = test + super(MockHandler, self).__init__() + + def get_response(self, path, request): + # debug("mock handler answering %s, %s", path, request) + return HttpResponse(self.test(path, request)) + + +def debug(*arg): + pass + # msg, arg = arg[0], arg[1:] + # print msg % arg + + +def start_response(code, headers): + debug("start response: %s %s", code, headers) + pass + +# tests +class TestRequestIsolation(unittest.TestCase): + + def setUp(self): + debug("setup") + self.settings = settings._target + settings._target = UserSettingsHolder(copy.deepcopy(settings._target)) + settings.OTHER_DATABASES['_a']['MODELS'] = ['ri.MX'] + settings.OTHER_DATABASES['_b']['MODELS'] = ['ri.MY'] + + def tearDown(self): + debug("teardown") + settings._target = self.settings + + def testRequestIsolation(self): + env = os.environ.copy() + env['PATH_INFO'] = '/' + env['REQUEST_METHOD'] = 'GET' + + def request_one(path, request): + """Start out with settings as originally configured""" + self.assertEqual(model_connection_name(MX), '_a') + self.assertEqual( + MX._default_manager.db.connection.settings.DATABASE_NAME, + settings.OTHER_DATABASES['_a']['DATABASE_NAME']) + self.assertEqual(model_connection_name(MY), '_b') + self.assertEqual( + MY._default_manager.db.connection.settings.DATABASE_NAME, + settings.OTHER_DATABASES['_b']['DATABASE_NAME']) + + def request_two(path, request): + """Between the first and second requests, settings change to assign + model MY to a different connection + """ + self.assertEqual(model_connection_name(MX), '_a') + self.assertEqual( + MX._default_manager.db.connection.settings.DATABASE_NAME, + settings.OTHER_DATABASES['_a']['DATABASE_NAME']) + self.assertEqual(model_connection_name(MY), _default) + self.assertEqual( + MY._default_manager.db.connection.settings.DATABASE_NAME, + settings.DATABASE_NAME) + + def request_three(path, request): + """Between the 2nd and 3rd requests, the settings at the names in + OTHER_DATABASES have changed. + """ + self.assertEqual(model_connection_name(MX), '_b') + self.assertEqual( + MX._default_manager.db.connection.settings.DATABASE_NAME, + settings.OTHER_DATABASES['_b']['DATABASE_NAME']) + self.assertEqual(model_connection_name(MY), '_a') + self.assertEqual( + MY._default_manager.db.connection.settings.DATABASE_NAME, + settings.OTHER_DATABASES['_a']['DATABASE_NAME']) + + MockHandler(request_one)(env, start_response) + + settings.OTHER_DATABASES['_b']['MODELS'] = [] + MockHandler(request_two)(env, start_response) + + settings.OTHER_DATABASES['_b']['MODELS'] = ['ri.MY'] + settings.OTHER_DATABASES['_b'], \ + settings.OTHER_DATABASES['_a'] = \ + settings.OTHER_DATABASES['_a'], \ + settings.OTHER_DATABASES['_b'] + MockHandler(request_three)(env, start_response) diff --git a/tests/regressiontests/string_lookup/models.py b/tests/regressiontests/string_lookup/models.py index a4582ca4e9..441bb3f8a3 100644 --- a/tests/regressiontests/string_lookup/models.py +++ b/tests/regressiontests/string_lookup/models.py @@ -34,7 +34,7 @@ class Base(models.Model): def __str__(self): return "Base %s" % self.name -API_TESTS = """ +__test__ = {'API_TESTS':""" # Regression test for #1661 and #1662: Check that string form referencing of models works, # both as pre and post reference, on all RelatedField types. @@ -66,4 +66,4 @@ API_TESTS = """ >>> child1.parent -""" +"""} diff --git a/tests/regressiontests/templates/__init__.py b/tests/regressiontests/templates/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/templates/models.py b/tests/regressiontests/templates/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py new file mode 100644 index 0000000000..2d1ce192ef --- /dev/null +++ b/tests/regressiontests/templates/tests.py @@ -0,0 +1,621 @@ +from django.conf import settings + +if __name__ == '__main__': + # When running this file in isolation, we need to set up the configuration + # before importing 'template'. + settings.configure() + +from django import template +from django.template import loader +from django.utils.translation import activate, deactivate, install +from django.utils.tzinfo import LocalTimezone +from datetime import datetime, timedelta +import unittest + +################################# +# Custom template tag for tests # +################################# + +register = template.Library() + +class EchoNode(template.Node): + def __init__(self, contents): + self.contents = contents + + def render(self, context): + return " ".join(self.contents) + +def do_echo(parser, token): + return EchoNode(token.contents.split()[1:]) + +register.tag("echo", do_echo) + +template.libraries['django.templatetags.testtags'] = register + +##################################### +# Helper objects for template tests # +##################################### + +class SomeException(Exception): + silent_variable_failure = True + +class SomeOtherException(Exception): + pass + +class SomeClass: + def __init__(self): + self.otherclass = OtherClass() + + def method(self): + return "SomeClass.method" + + def method2(self, o): + return o + + def method3(self): + raise SomeException + + def method4(self): + raise SomeOtherException + +class OtherClass: + def method(self): + return "OtherClass.method" + +class Templates(unittest.TestCase): + def test_templates(self): + # NOW and NOW_tz are used by timesince tag tests. + NOW = datetime.now() + NOW_tz = datetime.now(LocalTimezone(datetime.now())) + + # SYNTAX -- + # 'template_name': ('template contents', 'context dict', 'expected string output' or Exception class) + TEMPLATE_TESTS = { + + ### BASIC SYNTAX ########################################################## + + # Plain text should go through the template parser untouched + 'basic-syntax01': ("something cool", {}, "something cool"), + + # Variables should be replaced with their value in the current context + 'basic-syntax02': ("{{ headline }}", {'headline':'Success'}, "Success"), + + # More than one replacement variable is allowed in a template + 'basic-syntax03': ("{{ first }} --- {{ second }}", {"first" : 1, "second" : 2}, "1 --- 2"), + + # Fail silently when a variable is not found in the current context + 'basic-syntax04': ("as{{ missing }}df", {}, "asINVALIDdf"), + + # A variable may not contain more than one word + 'basic-syntax06': ("{{ multi word variable }}", {}, template.TemplateSyntaxError), + + # Raise TemplateSyntaxError for empty variable tags + 'basic-syntax07': ("{{ }}", {}, template.TemplateSyntaxError), + 'basic-syntax08': ("{{ }}", {}, template.TemplateSyntaxError), + + # Attribute syntax allows a template to call an object's attribute + 'basic-syntax09': ("{{ var.method }}", {"var": SomeClass()}, "SomeClass.method"), + + # Multiple levels of attribute access are allowed + 'basic-syntax10': ("{{ var.otherclass.method }}", {"var": SomeClass()}, "OtherClass.method"), + + # Fail silently when a variable's attribute isn't found + 'basic-syntax11': ("{{ var.blech }}", {"var": SomeClass()}, "INVALID"), + + # Raise TemplateSyntaxError when trying to access a variable beginning with an underscore + 'basic-syntax12': ("{{ var.__dict__ }}", {"var": SomeClass()}, template.TemplateSyntaxError), + + # Raise TemplateSyntaxError when trying to access a variable containing an illegal character + 'basic-syntax13': ("{{ va>r }}", {}, template.TemplateSyntaxError), + 'basic-syntax14': ("{{ (var.r) }}", {}, template.TemplateSyntaxError), + 'basic-syntax15': ("{{ sp%am }}", {}, template.TemplateSyntaxError), + 'basic-syntax16': ("{{ eggs! }}", {}, template.TemplateSyntaxError), + 'basic-syntax17': ("{{ moo? }}", {}, template.TemplateSyntaxError), + + # Attribute syntax allows a template to call a dictionary key's value + 'basic-syntax18': ("{{ foo.bar }}", {"foo" : {"bar" : "baz"}}, "baz"), + + # Fail silently when a variable's dictionary key isn't found + 'basic-syntax19': ("{{ foo.spam }}", {"foo" : {"bar" : "baz"}}, "INVALID"), + + # Fail silently when accessing a non-simple method + 'basic-syntax20': ("{{ var.method2 }}", {"var": SomeClass()}, "INVALID"), + + # Basic filter usage + 'basic-syntax21': ("{{ var|upper }}", {"var": "Django is the greatest!"}, "DJANGO IS THE GREATEST!"), + + # Chained filters + 'basic-syntax22': ("{{ var|upper|lower }}", {"var": "Django is the greatest!"}, "django is the greatest!"), + + # Raise TemplateSyntaxError for space between a variable and filter pipe + 'basic-syntax23': ("{{ var |upper }}", {}, template.TemplateSyntaxError), + + # Raise TemplateSyntaxError for space after a filter pipe + 'basic-syntax24': ("{{ var| upper }}", {}, template.TemplateSyntaxError), + + # Raise TemplateSyntaxError for a nonexistent filter + 'basic-syntax25': ("{{ var|does_not_exist }}", {}, template.TemplateSyntaxError), + + # Raise TemplateSyntaxError when trying to access a filter containing an illegal character + 'basic-syntax26': ("{{ var|fil(ter) }}", {}, template.TemplateSyntaxError), + + # Raise TemplateSyntaxError for invalid block tags + 'basic-syntax27': ("{% nothing_to_see_here %}", {}, template.TemplateSyntaxError), + + # Raise TemplateSyntaxError for empty block tags + 'basic-syntax28': ("{% %}", {}, template.TemplateSyntaxError), + + # Chained filters, with an argument to the first one + 'basic-syntax29': ('{{ var|removetags:"b i"|upper|lower }}', {"var": "Yes"}, "yes"), + + # Escaped string as argument + 'basic-syntax30': (r'{{ var|default_if_none:" endquote\" hah" }}', {"var": None}, ' endquote" hah'), + + # Variable as argument + 'basic-syntax31': (r'{{ var|default_if_none:var2 }}', {"var": None, "var2": "happy"}, 'happy'), + + # Default argument testing + 'basic-syntax32': (r'{{ var|yesno:"yup,nup,mup" }} {{ var|yesno }}', {"var": True}, 'yup yes'), + + # Fail silently for methods that raise an exception with a "silent_variable_failure" attribute + 'basic-syntax33': (r'1{{ var.method3 }}2', {"var": SomeClass()}, "1INVALID2"), + + # In methods that raise an exception without a "silent_variable_attribute" set to True, + # the exception propogates + 'basic-syntax34': (r'1{{ var.method4 }}2', {"var": SomeClass()}, SomeOtherException), + + # Escaped backslash in argument + 'basic-syntax35': (r'{{ var|default_if_none:"foo\bar" }}', {"var": None}, r'foo\bar'), + + # Escaped backslash using known escape char + 'basic-syntax35': (r'{{ var|default_if_none:"foo\now" }}', {"var": None}, r'foo\now'), + + ### COMMENT TAG ########################################################### + 'comment-tag01': ("{% comment %}this is hidden{% endcomment %}hello", {}, "hello"), + 'comment-tag02': ("{% comment %}this is hidden{% endcomment %}hello{% comment %}foo{% endcomment %}", {}, "hello"), + + # Comment tag can contain invalid stuff. + 'comment-tag03': ("foo{% comment %} {% if %} {% endcomment %}", {}, "foo"), + 'comment-tag04': ("foo{% comment %} {% endblock %} {% endcomment %}", {}, "foo"), + 'comment-tag05': ("foo{% comment %} {% somerandomtag %} {% endcomment %}", {}, "foo"), + + ### CYCLE TAG ############################################################# + 'cycle01': ('{% cycle a %}', {}, template.TemplateSyntaxError), + 'cycle02': ('{% cycle a,b,c as abc %}{% cycle abc %}', {}, 'ab'), + 'cycle03': ('{% cycle a,b,c as abc %}{% cycle abc %}{% cycle abc %}', {}, 'abc'), + 'cycle04': ('{% cycle a,b,c as abc %}{% cycle abc %}{% cycle abc %}{% cycle abc %}', {}, 'abca'), + 'cycle05': ('{% cycle %}', {}, template.TemplateSyntaxError), + 'cycle06': ('{% cycle a %}', {}, template.TemplateSyntaxError), + 'cycle07': ('{% cycle a,b,c as foo %}{% cycle bar %}', {}, template.TemplateSyntaxError), + + ### EXCEPTIONS ############################################################ + + # Raise exception for invalid template name + 'exception01': ("{% extends 'nonexistent' %}", {}, template.TemplateSyntaxError), + + # Raise exception for invalid template name (in variable) + 'exception02': ("{% extends nonexistent %}", {}, template.TemplateSyntaxError), + + # Raise exception for extra {% extends %} tags + 'exception03': ("{% extends 'inheritance01' %}{% block first %}2{% endblock %}{% extends 'inheritance16' %}", {}, template.TemplateSyntaxError), + + # Raise exception for custom tags used in child with {% load %} tag in parent, not in child + 'exception04': ("{% extends 'inheritance17' %}{% block first %}{% echo 400 %}5678{% endblock %}", {}, template.TemplateSyntaxError), + + ### FILTER TAG ############################################################ + 'filter01': ('{% filter upper %}{% endfilter %}', {}, ''), + 'filter02': ('{% filter upper %}django{% endfilter %}', {}, 'DJANGO'), + 'filter03': ('{% filter upper|lower %}django{% endfilter %}', {}, 'django'), + + ### FIRSTOF TAG ########################################################### + 'firstof01': ('{% firstof a b c %}', {'a':0,'b':0,'c':0}, ''), + 'firstof02': ('{% firstof a b c %}', {'a':1,'b':0,'c':0}, '1'), + 'firstof03': ('{% firstof a b c %}', {'a':0,'b':2,'c':0}, '2'), + 'firstof04': ('{% firstof a b c %}', {'a':0,'b':0,'c':3}, '3'), + 'firstof05': ('{% firstof a b c %}', {'a':1,'b':2,'c':3}, '1'), + 'firstof06': ('{% firstof %}', {}, template.TemplateSyntaxError), + + ### FOR TAG ############################################################### + 'for-tag01': ("{% for val in values %}{{ val }}{% endfor %}", {"values": [1, 2, 3]}, "123"), + 'for-tag02': ("{% for val in values reversed %}{{ val }}{% endfor %}", {"values": [1, 2, 3]}, "321"), + 'for-tag-vars01': ("{% for val in values %}{{ forloop.counter }}{% endfor %}", {"values": [6, 6, 6]}, "123"), + 'for-tag-vars02': ("{% for val in values %}{{ forloop.counter0 }}{% endfor %}", {"values": [6, 6, 6]}, "012"), + 'for-tag-vars03': ("{% for val in values %}{{ forloop.revcounter }}{% endfor %}", {"values": [6, 6, 6]}, "321"), + 'for-tag-vars04': ("{% for val in values %}{{ forloop.revcounter0 }}{% endfor %}", {"values": [6, 6, 6]}, "210"), + + ### IF TAG ################################################################ + 'if-tag01': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": True}, "yes"), + 'if-tag02': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": False}, "no"), + 'if-tag03': ("{% if foo %}yes{% else %}no{% endif %}", {}, "no"), + + # AND + 'if-tag-and01': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'yes'), + 'if-tag-and02': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'no'), + 'if-tag-and03': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'no'), + 'if-tag-and04': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'no'), + 'if-tag-and05': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': False}, 'no'), + 'if-tag-and06': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'bar': False}, 'no'), + 'if-tag-and07': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': True}, 'no'), + 'if-tag-and08': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'bar': True}, 'no'), + + # OR + 'if-tag-or01': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'yes'), + 'if-tag-or02': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'yes'), + 'if-tag-or03': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'yes'), + 'if-tag-or04': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'no'), + 'if-tag-or05': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': False}, 'no'), + 'if-tag-or06': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'bar': False}, 'no'), + 'if-tag-or07': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': True}, 'yes'), + 'if-tag-or08': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'bar': True}, 'yes'), + + # TODO: multiple ORs + + # NOT + 'if-tag-not01': ("{% if not foo %}no{% else %}yes{% endif %}", {'foo': True}, 'yes'), + 'if-tag-not02': ("{% if not %}yes{% else %}no{% endif %}", {'foo': True}, 'no'), + 'if-tag-not03': ("{% if not %}yes{% else %}no{% endif %}", {'not': True}, 'yes'), + 'if-tag-not04': ("{% if not not %}no{% else %}yes{% endif %}", {'not': True}, 'yes'), + 'if-tag-not05': ("{% if not not %}no{% else %}yes{% endif %}", {}, 'no'), + + 'if-tag-not06': ("{% if foo and not bar %}yes{% else %}no{% endif %}", {}, 'no'), + 'if-tag-not07': ("{% if foo and not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'no'), + 'if-tag-not08': ("{% if foo and not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'yes'), + 'if-tag-not09': ("{% if foo and not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'no'), + 'if-tag-not10': ("{% if foo and not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'no'), + + 'if-tag-not11': ("{% if not foo and bar %}yes{% else %}no{% endif %}", {}, 'no'), + 'if-tag-not12': ("{% if not foo and bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'no'), + 'if-tag-not13': ("{% if not foo and bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'no'), + 'if-tag-not14': ("{% if not foo and bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'yes'), + 'if-tag-not15': ("{% if not foo and bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'no'), + + 'if-tag-not16': ("{% if foo or not bar %}yes{% else %}no{% endif %}", {}, 'yes'), + 'if-tag-not17': ("{% if foo or not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'yes'), + 'if-tag-not18': ("{% if foo or not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'yes'), + 'if-tag-not19': ("{% if foo or not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'no'), + 'if-tag-not20': ("{% if foo or not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'yes'), + + 'if-tag-not21': ("{% if not foo or bar %}yes{% else %}no{% endif %}", {}, 'yes'), + 'if-tag-not22': ("{% if not foo or bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'yes'), + 'if-tag-not23': ("{% if not foo or bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'no'), + 'if-tag-not24': ("{% if not foo or bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'yes'), + 'if-tag-not25': ("{% if not foo or bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'yes'), + + 'if-tag-not26': ("{% if not foo and not bar %}yes{% else %}no{% endif %}", {}, 'yes'), + 'if-tag-not27': ("{% if not foo and not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'no'), + 'if-tag-not28': ("{% if not foo and not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'no'), + 'if-tag-not29': ("{% if not foo and not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'no'), + 'if-tag-not30': ("{% if not foo and not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'yes'), + + 'if-tag-not31': ("{% if not foo or not bar %}yes{% else %}no{% endif %}", {}, 'yes'), + 'if-tag-not32': ("{% if not foo or not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'no'), + 'if-tag-not33': ("{% if not foo or not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'yes'), + 'if-tag-not34': ("{% if not foo or not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'yes'), + 'if-tag-not35': ("{% if not foo or not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'yes'), + + # AND and OR raises a TemplateSyntaxError + 'if-tag-error01': ("{% if foo or bar and baz %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, template.TemplateSyntaxError), + 'if-tag-error02': ("{% if foo and %}yes{% else %}no{% endif %}", {'foo': True}, template.TemplateSyntaxError), + 'if-tag-error03': ("{% if foo or %}yes{% else %}no{% endif %}", {'foo': True}, template.TemplateSyntaxError), + 'if-tag-error04': ("{% if not foo and %}yes{% else %}no{% endif %}", {'foo': True}, template.TemplateSyntaxError), + 'if-tag-error05': ("{% if not foo or %}yes{% else %}no{% endif %}", {'foo': True}, template.TemplateSyntaxError), + + ### IFCHANGED TAG ######################################################### + 'ifchanged01': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', { 'num': (1,2,3) }, '123'), + 'ifchanged02': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', { 'num': (1,1,3) }, '13'), + 'ifchanged03': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', { 'num': (1,1,1) }, '1'), + + ### IFEQUAL TAG ########################################################### + 'ifequal01': ("{% ifequal a b %}yes{% endifequal %}", {"a": 1, "b": 2}, ""), + 'ifequal02': ("{% ifequal a b %}yes{% endifequal %}", {"a": 1, "b": 1}, "yes"), + 'ifequal03': ("{% ifequal a b %}yes{% else %}no{% endifequal %}", {"a": 1, "b": 2}, "no"), + 'ifequal04': ("{% ifequal a b %}yes{% else %}no{% endifequal %}", {"a": 1, "b": 1}, "yes"), + 'ifequal05': ("{% ifequal a 'test' %}yes{% else %}no{% endifequal %}", {"a": "test"}, "yes"), + 'ifequal06': ("{% ifequal a 'test' %}yes{% else %}no{% endifequal %}", {"a": "no"}, "no"), + 'ifequal07': ('{% ifequal a "test" %}yes{% else %}no{% endifequal %}', {"a": "test"}, "yes"), + 'ifequal08': ('{% ifequal a "test" %}yes{% else %}no{% endifequal %}', {"a": "no"}, "no"), + 'ifequal09': ('{% ifequal a "test" %}yes{% else %}no{% endifequal %}', {}, "no"), + 'ifequal10': ('{% ifequal a b %}yes{% else %}no{% endifequal %}', {}, "yes"), + + # SMART SPLITTING + 'ifequal-split01': ('{% ifequal a "test man" %}yes{% else %}no{% endifequal %}', {}, "no"), + 'ifequal-split02': ('{% ifequal a "test man" %}yes{% else %}no{% endifequal %}', {'a': 'foo'}, "no"), + 'ifequal-split03': ('{% ifequal a "test man" %}yes{% else %}no{% endifequal %}', {'a': 'test man'}, "yes"), + 'ifequal-split04': ("{% ifequal a 'test man' %}yes{% else %}no{% endifequal %}", {'a': 'test man'}, "yes"), + 'ifequal-split05': ("{% ifequal a 'i \"love\" you' %}yes{% else %}no{% endifequal %}", {'a': ''}, "no"), + 'ifequal-split06': ("{% ifequal a 'i \"love\" you' %}yes{% else %}no{% endifequal %}", {'a': 'i "love" you'}, "yes"), + 'ifequal-split07': ("{% ifequal a 'i \"love\" you' %}yes{% else %}no{% endifequal %}", {'a': 'i love you'}, "no"), + 'ifequal-split08': (r"{% ifequal a 'I\'m happy' %}yes{% else %}no{% endifequal %}", {'a': "I'm happy"}, "yes"), + 'ifequal-split09': (r"{% ifequal a 'slash\man' %}yes{% else %}no{% endifequal %}", {'a': r"slash\man"}, "yes"), + 'ifequal-split10': (r"{% ifequal a 'slash\man' %}yes{% else %}no{% endifequal %}", {'a': r"slashman"}, "no"), + + ### IFNOTEQUAL TAG ######################################################## + 'ifnotequal01': ("{% ifnotequal a b %}yes{% endifnotequal %}", {"a": 1, "b": 2}, "yes"), + 'ifnotequal02': ("{% ifnotequal a b %}yes{% endifnotequal %}", {"a": 1, "b": 1}, ""), + 'ifnotequal03': ("{% ifnotequal a b %}yes{% else %}no{% endifnotequal %}", {"a": 1, "b": 2}, "yes"), + 'ifnotequal04': ("{% ifnotequal a b %}yes{% else %}no{% endifnotequal %}", {"a": 1, "b": 1}, "no"), + + ### INCLUDE TAG ########################################################### + 'include01': ('{% include "basic-syntax01" %}', {}, "something cool"), + 'include02': ('{% include "basic-syntax02" %}', {'headline': 'Included'}, "Included"), + 'include03': ('{% include template_name %}', {'template_name': 'basic-syntax02', 'headline': 'Included'}, "Included"), + 'include04': ('a{% include "nonexistent" %}b', {}, "ab"), + + ### INHERITANCE ########################################################### + + # Standard template with no inheritance + 'inheritance01': ("1{% block first %}_{% endblock %}3{% block second %}_{% endblock %}", {}, '1_3_'), + + # Standard two-level inheritance + 'inheritance02': ("{% extends 'inheritance01' %}{% block first %}2{% endblock %}{% block second %}4{% endblock %}", {}, '1234'), + + # Three-level with no redefinitions on third level + 'inheritance03': ("{% extends 'inheritance02' %}", {}, '1234'), + + # Two-level with no redefinitions on second level + 'inheritance04': ("{% extends 'inheritance01' %}", {}, '1_3_'), + + # Two-level with double quotes instead of single quotes + 'inheritance05': ('{% extends "inheritance02" %}', {}, '1234'), + + # Three-level with variable parent-template name + 'inheritance06': ("{% extends foo %}", {'foo': 'inheritance02'}, '1234'), + + # Two-level with one block defined, one block not defined + 'inheritance07': ("{% extends 'inheritance01' %}{% block second %}5{% endblock %}", {}, '1_35'), + + # Three-level with one block defined on this level, two blocks defined next level + 'inheritance08': ("{% extends 'inheritance02' %}{% block second %}5{% endblock %}", {}, '1235'), + + # Three-level with second and third levels blank + 'inheritance09': ("{% extends 'inheritance04' %}", {}, '1_3_'), + + # Three-level with space NOT in a block -- should be ignored + 'inheritance10': ("{% extends 'inheritance04' %} ", {}, '1_3_'), + + # Three-level with both blocks defined on this level, but none on second level + 'inheritance11': ("{% extends 'inheritance04' %}{% block first %}2{% endblock %}{% block second %}4{% endblock %}", {}, '1234'), + + # Three-level with this level providing one and second level providing the other + 'inheritance12': ("{% extends 'inheritance07' %}{% block first %}2{% endblock %}", {}, '1235'), + + # Three-level with this level overriding second level + 'inheritance13': ("{% extends 'inheritance02' %}{% block first %}a{% endblock %}{% block second %}b{% endblock %}", {}, '1a3b'), + + # A block defined only in a child template shouldn't be displayed + 'inheritance14': ("{% extends 'inheritance01' %}{% block newblock %}NO DISPLAY{% endblock %}", {}, '1_3_'), + + # A block within another block + 'inheritance15': ("{% extends 'inheritance01' %}{% block first %}2{% block inner %}inner{% endblock %}{% endblock %}", {}, '12inner3_'), + + # A block within another block (level 2) + 'inheritance16': ("{% extends 'inheritance15' %}{% block inner %}out{% endblock %}", {}, '12out3_'), + + # {% load %} tag (parent -- setup for exception04) + 'inheritance17': ("{% load testtags %}{% block first %}1234{% endblock %}", {}, '1234'), + + # {% load %} tag (standard usage, without inheritance) + 'inheritance18': ("{% load testtags %}{% echo this that theother %}5678", {}, 'this that theother5678'), + + # {% load %} tag (within a child template) + 'inheritance19': ("{% extends 'inheritance01' %}{% block first %}{% load testtags %}{% echo 400 %}5678{% endblock %}", {}, '140056783_'), + + # Two-level inheritance with {{ block.super }} + 'inheritance20': ("{% extends 'inheritance01' %}{% block first %}{{ block.super }}a{% endblock %}", {}, '1_a3_'), + + # Three-level inheritance with {{ block.super }} from parent + 'inheritance21': ("{% extends 'inheritance02' %}{% block first %}{{ block.super }}a{% endblock %}", {}, '12a34'), + + # Three-level inheritance with {{ block.super }} from grandparent + 'inheritance22': ("{% extends 'inheritance04' %}{% block first %}{{ block.super }}a{% endblock %}", {}, '1_a3_'), + + # Three-level inheritance with {{ block.super }} from parent and grandparent + 'inheritance23': ("{% extends 'inheritance20' %}{% block first %}{{ block.super }}b{% endblock %}", {}, '1_ab3_'), + + # Inheritance from local context without use of template loader + 'inheritance24': ("{% extends context_template %}{% block first %}2{% endblock %}{% block second %}4{% endblock %}", {'context_template': template.Template("1{% block first %}_{% endblock %}3{% block second %}_{% endblock %}")}, '1234'), + + # Inheritance from local context with variable parent template + 'inheritance25': ("{% extends context_template.1 %}{% block first %}2{% endblock %}{% block second %}4{% endblock %}", {'context_template': [template.Template("Wrong"), template.Template("1{% block first %}_{% endblock %}3{% block second %}_{% endblock %}")]}, '1234'), + + ### I18N ################################################################## + + # {% spaceless %} tag + 'spaceless01': ("{% spaceless %} text {% endspaceless %}", {}, " text "), + 'spaceless02': ("{% spaceless %} \n text \n {% endspaceless %}", {}, " text "), + 'spaceless03': ("{% spaceless %}text{% endspaceless %}", {}, "text"), + + # simple translation of a string delimited by ' + 'i18n01': ("{% load i18n %}{% trans 'xxxyyyxxx' %}", {}, "xxxyyyxxx"), + + # simple translation of a string delimited by " + 'i18n02': ('{% load i18n %}{% trans "xxxyyyxxx" %}', {}, "xxxyyyxxx"), + + # simple translation of a variable + 'i18n03': ('{% load i18n %}{% blocktrans %}{{ anton }}{% endblocktrans %}', {'anton': 'xxxyyyxxx'}, "xxxyyyxxx"), + + # simple translation of a variable and filter + 'i18n04': ('{% load i18n %}{% blocktrans with anton|lower as berta %}{{ berta }}{% endblocktrans %}', {'anton': 'XXXYYYXXX'}, "xxxyyyxxx"), + + # simple translation of a string with interpolation + 'i18n05': ('{% load i18n %}{% blocktrans %}xxx{{ anton }}xxx{% endblocktrans %}', {'anton': 'yyy'}, "xxxyyyxxx"), + + # simple translation of a string to german + 'i18n06': ('{% load i18n %}{% trans "Page not found" %}', {'LANGUAGE_CODE': 'de'}, "Seite nicht gefunden"), + + # translation of singular form + 'i18n07': ('{% load i18n %}{% blocktrans count number as counter %}singular{% plural %}plural{% endblocktrans %}', {'number': 1}, "singular"), + + # translation of plural form + 'i18n08': ('{% load i18n %}{% blocktrans count number as counter %}singular{% plural %}plural{% endblocktrans %}', {'number': 2}, "plural"), + + # simple non-translation (only marking) of a string to german + 'i18n09': ('{% load i18n %}{% trans "Page not found" noop %}', {'LANGUAGE_CODE': 'de'}, "Page not found"), + + # translation of a variable with a translated filter + 'i18n10': ('{{ bool|yesno:_("ja,nein") }}', {'bool': True}, 'ja'), + + # translation of a variable with a non-translated filter + 'i18n11': ('{{ bool|yesno:"ja,nein" }}', {'bool': True}, 'ja'), + + # usage of the get_available_languages tag + 'i18n12': ('{% load i18n %}{% get_available_languages as langs %}{% for lang in langs %}{% ifequal lang.0 "de" %}{{ lang.0 }}{% endifequal %}{% endfor %}', {}, 'de'), + + # translation of a constant string + 'i18n13': ('{{ _("Page not found") }}', {'LANGUAGE_CODE': 'de'}, 'Seite nicht gefunden'), + + ### MULTILINE ############################################################# + + 'multiline01': (""" + Hello, + boys. + How + are + you + gentlemen. + """, + {}, + """ + Hello, + boys. + How + are + you + gentlemen. + """), + + ### REGROUP TAG ########################################################### + 'regroup01': ('{% regroup data by bar as grouped %}' + \ + '{% for group in grouped %}' + \ + '{{ group.grouper }}:' + \ + '{% for item in group.list %}' + \ + '{{ item.foo }}' + \ + '{% endfor %},' + \ + '{% endfor %}', + {'data': [ {'foo':'c', 'bar':1}, + {'foo':'d', 'bar':1}, + {'foo':'a', 'bar':2}, + {'foo':'b', 'bar':2}, + {'foo':'x', 'bar':3} ]}, + '1:cd,2:ab,3:x,'), + + # Test for silent failure when target variable isn't found + 'regroup02': ('{% regroup data by bar as grouped %}' + \ + '{% for group in grouped %}' + \ + '{{ group.grouper }}:' + \ + '{% for item in group.list %}' + \ + '{{ item.foo }}' + \ + '{% endfor %},' + \ + '{% endfor %}', + {}, 'INVALID:INVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALID,'), + + ### TEMPLATETAG TAG ####################################################### + 'templatetag01': ('{% templatetag openblock %}', {}, '{%'), + 'templatetag02': ('{% templatetag closeblock %}', {}, '%}'), + 'templatetag03': ('{% templatetag openvariable %}', {}, '{{'), + 'templatetag04': ('{% templatetag closevariable %}', {}, '}}'), + 'templatetag05': ('{% templatetag %}', {}, template.TemplateSyntaxError), + 'templatetag06': ('{% templatetag foo %}', {}, template.TemplateSyntaxError), + 'templatetag07': ('{% templatetag openbrace %}', {}, '{'), + 'templatetag08': ('{% templatetag closebrace %}', {}, '}'), + 'templatetag09': ('{% templatetag openbrace %}{% templatetag openbrace %}', {}, '{{'), + 'templatetag10': ('{% templatetag closebrace %}{% templatetag closebrace %}', {}, '}}'), + + ### WIDTHRATIO TAG ######################################################## + 'widthratio01': ('{% widthratio a b 0 %}', {'a':50,'b':100}, '0'), + 'widthratio02': ('{% widthratio a b 100 %}', {'a':0,'b':0}, ''), + 'widthratio03': ('{% widthratio a b 100 %}', {'a':0,'b':100}, '0'), + 'widthratio04': ('{% widthratio a b 100 %}', {'a':50,'b':100}, '50'), + 'widthratio05': ('{% widthratio a b 100 %}', {'a':100,'b':100}, '100'), + + # 62.5 should round to 63 + 'widthratio06': ('{% widthratio a b 100 %}', {'a':50,'b':80}, '63'), + + # 71.4 should round to 71 + 'widthratio07': ('{% widthratio a b 100 %}', {'a':50,'b':70}, '71'), + + # Raise exception if we don't have 3 args, last one an integer + 'widthratio08': ('{% widthratio %}', {}, template.TemplateSyntaxError), + 'widthratio09': ('{% widthratio a b %}', {'a':50,'b':100}, template.TemplateSyntaxError), + 'widthratio10': ('{% widthratio a b 100.0 %}', {'a':50,'b':100}, template.TemplateSyntaxError), + + ### NOW TAG ######################################################## + # Simple case + 'now01' : ('{% now "j n Y"%}', {}, str(datetime.now().day) + ' ' + str(datetime.now().month) + ' ' + str(datetime.now().year)), + + # Check parsing of escaped and special characters + 'now02' : ('{% now "j "n" Y"%}', {}, template.TemplateSyntaxError), + # 'now03' : ('{% now "j \"n\" Y"%}', {}, str(datetime.now().day) + '"' + str(datetime.now().month) + '"' + str(datetime.now().year)), + # 'now04' : ('{% now "j \nn\n Y"%}', {}, str(datetime.now().day) + '\n' + str(datetime.now().month) + '\n' + str(datetime.now().year)) + + ### TIMESINCE TAG ################################################## + # Default compare with datetime.now() + 'timesince01' : ('{{ a|timesince }}', {'a':datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1 minute'), + 'timesince02' : ('{{ a|timesince }}', {'a':(datetime.now() - timedelta(days=1, minutes = 1))}, '1 day'), + 'timesince03' : ('{{ a|timesince }}', {'a':(datetime.now() - + timedelta(hours=1, minutes=25, seconds = 10))}, '1 hour, 25 minutes'), + + # Compare to a given parameter + 'timesince04' : ('{{ a|timesince:b }}', {'a':NOW + timedelta(days=2), 'b':NOW + timedelta(days=1)}, '1 day'), + 'timesince05' : ('{{ a|timesince:b }}', {'a':NOW + timedelta(days=2, minutes=1), 'b':NOW + timedelta(days=2)}, '1 minute'), + + # Check that timezone is respected + 'timesince06' : ('{{ a|timesince:b }}', {'a':NOW_tz + timedelta(hours=8), 'b':NOW_tz}, '8 hours'), + + ### TIMEUNTIL TAG ################################################## + # Default compare with datetime.now() + 'timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2 minutes'), + 'timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1 day'), + 'timeuntil03' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(hours=8, minutes=10, seconds = 10))}, '8 hours, 10 minutes'), + + # Compare to a given parameter + 'timeuntil04' : ('{{ a|timeuntil:b }}', {'a':NOW - timedelta(days=1), 'b':NOW - timedelta(days=2)}, '1 day'), + 'timeuntil05' : ('{{ a|timeuntil:b }}', {'a':NOW - timedelta(days=2), 'b':NOW - timedelta(days=2, minutes=1)}, '1 minute'), + } + + # Register our custom template loader. + def test_template_loader(template_name, template_dirs=None): + "A custom template loader that loads the unit-test templates." + try: + return (TEMPLATE_TESTS[template_name][0] , "test:%s" % template_name) + except KeyError: + raise template.TemplateDoesNotExist, template_name + + old_template_loaders = loader.template_source_loaders + loader.template_source_loaders = [test_template_loader] + + failures = [] + tests = TEMPLATE_TESTS.items() + tests.sort() + + # Turn TEMPLATE_DEBUG off, because tests assume that. + old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False + + # Set TEMPLATE_STRING_IF_INVALID to a known string + old_invalid, settings.TEMPLATE_STRING_IF_INVALID = settings.TEMPLATE_STRING_IF_INVALID, 'INVALID' + + for name, vals in tests: + install() + if 'LANGUAGE_CODE' in vals[1]: + activate(vals[1]['LANGUAGE_CODE']) + else: + activate('en-us') + try: + output = loader.get_template(name).render(template.Context(vals[1])) + except Exception, e: + if e.__class__ != vals[2]: + failures.append("Template test: %s -- FAILED. Got %s, exception: %s" % (name, e.__class__, e)) + continue + if 'LANGUAGE_CODE' in vals[1]: + deactivate() + if output != vals[2]: + failures.append("Template test: %s -- FAILED. Expected %r, got %r" % (name, vals[2], output)) + loader.template_source_loaders = old_template_loaders + deactivate() + settings.TEMPLATE_DEBUG = old_td + settings.TEMPLATE_STRING_IF_INVALID = old_invalid + + self.assertEqual(failures, []) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/regressiontests/thread_isolation/__init__.py b/tests/regressiontests/thread_isolation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/thread_isolation/models.py b/tests/regressiontests/thread_isolation/models.py new file mode 100644 index 0000000000..5defcf5a70 --- /dev/null +++ b/tests/regressiontests/thread_isolation/models.py @@ -0,0 +1,19 @@ +from django.db import models + +# models +class MQ(models.Model): + val = models.CharField(maxlength=10) + class Meta: + app_label = 'ti' + + +class MX(models.Model): + val = models.CharField(maxlength=10) + class Meta: + app_label = 'ti' + + +class MY(models.Model): + val = models.CharField(maxlength=10) + class Meta: + app_label = 'ti' diff --git a/tests/regressiontests/thread_isolation/tests.py b/tests/regressiontests/thread_isolation/tests.py new file mode 100644 index 0000000000..f677c27bf8 --- /dev/null +++ b/tests/regressiontests/thread_isolation/tests.py @@ -0,0 +1,251 @@ +# tests that db settings can be different in different threads +# +# +# What's going on here: +# +# Simulating multiple web requests in a threaded environment, one in +# which settings are different for each request. So we replace +# django.conf.settings with a thread local, with different +# configurations in each thread, and then fire off three +# simultaneous requests (using a condition to sync them up), and +# test that each thread sees its own settings and the models in each +# thread attempt to connect to the correct database as per their +# settings. +# + + +import copy +import os +import sys +import threading +import unittest +from thread import get_ident + +from django.conf import settings, UserSettingsHolder +from django.core.handlers.wsgi import WSGIHandler +from django.db import model_connection_name, _default, connection, connections +from regressiontests.request_isolation.tests import MockHandler +from regressiontests.thread_isolation.models import * + +try: + # Only exists in Python 2.4+ + from threading import local +except ImportError: + # Import copy of _thread_local.py from Python 2.4 + from django.utils._threading_local import local + + +# helpers +class LocalSettings: + """Settings holder that allows thread-local overrides of defaults. + """ + def __init__(self, defaults): + self._defaults = defaults + self._local = local() + + def __getattr__(self, attr): + if attr in ('_defaults', '_local'): + return self.__dict__[attr] + _local = self.__dict__['_local'] + _defaults = self.__dict__['_defaults'] + debug("LS get %s (%s)", attr, hasattr(_local, attr)) + if not hasattr(_local, attr): + # Make sure everything we return is the local version; this + # avoids sets to deep datastructures overwriting the defaults + setattr(_local, attr, copy.deepcopy(getattr(_defaults, attr))) + return getattr(_local, attr) + + def __setattr__(self, attr, val): + if attr in ('_defaults', '_local'): + self.__dict__[attr] = val + else: + debug("LS set local %s = %s", attr, val) + setattr(self.__dict__['_local'], attr, val) + +def thread_two(func, *arg): + def start(): + # from django.conf import settings + settings.OTHER_DATABASES['_b']['MODELS'] = [] + + debug("t2 ODB: %s", settings.OTHER_DATABASES) + debug("t2 waiting") + ev.wait(2.0) + func(*arg) + debug("t2 complete") + t2 = threading.Thread(target=start) + t2.start() + return t2 + +def thread_three(func, *arg): + def start(): + # from django.conf import settings + settings.OTHER_DATABASES['_b']['MODELS'] = ['ti.MY'] + settings.OTHER_DATABASES['_b'], \ + settings.OTHER_DATABASES['_a'] = \ + settings.OTHER_DATABASES['_a'], \ + settings.OTHER_DATABASES['_b'] + + settings.DATABASE_NAME = \ + settings.OTHER_DATABASES['_a']['DATABASE_NAME'] + + debug("t3 ODB: %s", settings.OTHER_DATABASES) + debug("3 %s: start: default: %s", get_ident(), settings.DATABASE_NAME) + debug("3 %s: start: conn: %s", get_ident(), + connection.settings.DATABASE_NAME) + + debug("t3 waiting") + ev.wait(2.0) + func(*arg) + debug("t3 complete") + t3 = threading.Thread(target=start) + t3.start() + return t3 + +def debug(*arg): + pass +# msg, arg = arg[0], arg[1:] +# print msg % arg + +def start_response(code, headers): + debug("start response: %s %s", code, headers) + pass + +class TestThreadIsolation(unittest.TestCase): + # event used to synchronize threads so we can be sure they are running + # together + ev = threading.Event() + lock = threading.RLock() + errors = [] + + def setUp(self): + debug("setup") + self.settings = settings._target + settings._target = UserSettingsHolder(copy.deepcopy(settings._target)) + settings.OTHER_DATABASES['_a']['MODELS'] = ['ti.MX'] + settings.OTHER_DATABASES['_b']['MODELS'] = ['ti.MY'] + + # normal settings holders aren't thread-safe, so we need to substitute + # one that is (and so allows per-thread settings) + holder = settings._target + settings._target = LocalSettings(holder) + + def teardown(self): + debug("teardown") + settings._target = self.settings + + def add_thread_error(self, err): + self.lock.acquire() + try: + self.errors.append(err) + finally: + self.lock.release() + + def thread_errors(self): + self.lock.acquire() + try: + return self.errors[:] + finally: + self.lock.release() + + def request_one(self, path, request): + """Start out with settings as originally configured""" + from django.conf import settings + debug("request_one: %s", settings.OTHER_DATABASES) + + self.assertEqual(model_connection_name(MQ), _default) + self.assertEqual(model_connection_name(MX), '_a') + self.assertEqual( + MX._default_manager.db.connection.settings.DATABASE_NAME, + settings.OTHER_DATABASES['_a']['DATABASE_NAME']) + self.assertEqual(model_connection_name(MY), '_b') + self.assertEqual( + MY._default_manager.db.connection.settings.DATABASE_NAME, + settings.OTHER_DATABASES['_b']['DATABASE_NAME']) + self.assert_(MQ._default_manager.db.connection is + connections[_default].connection) + self.assertEqual( + MQ._default_manager.db.connection.settings.DATABASE_NAME, + settings.DATABASE_NAME) + self.assertEqual(connection.settings.DATABASE_NAME, + settings.DATABASE_NAME) + + def request_two(self, path, request): + """Between the first and second requests, settings change to assign + model MY to a different connection + """ + # from django.conf import settings + debug("request_two: %s", settings.OTHER_DATABASES) + + try: + self.assertEqual(model_connection_name(MQ), _default) + self.assertEqual(model_connection_name(MX), '_a') + self.assertEqual( + MX._default_manager.db.connection.settings.DATABASE_NAME, + settings.OTHER_DATABASES['_a']['DATABASE_NAME']) + self.assertEqual(model_connection_name(MY), _default) + self.assertEqual( + MY._default_manager.db.connection.settings.DATABASE_NAME, + settings.DATABASE_NAME) + self.assert_(MQ._default_manager.db.connection is + connections[_default].connection) + self.assertEqual( + MQ._default_manager.db.connection.settings.DATABASE_NAME, + settings.DATABASE_NAME) + self.assertEqual(connection.settings.DATABASE_NAME, + settings.DATABASE_NAME) + except: + self.add_thread_error(sys.exc_info()) + + def request_three(self, path, request): + """Between the 2nd and 3rd requests, the settings at the names in + OTHER_DATABASES have changed. + """ + # from django.conf import settings + debug("3 %s: %s", get_ident(), settings.OTHER_DATABASES) + debug("3 %s: default: %s", get_ident(), settings.DATABASE_NAME) + debug("3 %s: conn: %s", get_ident(), + connection.settings.DATABASE_NAME) + try: + self.assertEqual(model_connection_name(MQ), _default) + self.assertEqual(model_connection_name(MX), '_b') + self.assertEqual( + MX._default_manager.db.connection.settings.DATABASE_NAME, + settings.OTHER_DATABASES['_b']['DATABASE_NAME']) + self.assertEqual(model_connection_name(MY), '_a') + self.assertEqual( + MY._default_manager.db.connection.settings.DATABASE_NAME, + settings.OTHER_DATABASES['_a']['DATABASE_NAME']) + self.assert_(MQ._default_manager.db.connection is + connections[_default].connection) + self.assertEqual( + connection.settings.DATABASE_NAME, + settings.OTHER_DATABASES['_a']['DATABASE_NAME']) + except: + self.add_thread_error(sys.exc_info()) + + def test_thread_isolation(self): + + debug("running tests") + + env = os.environ.copy() + env['PATH_INFO'] = '/' + env['REQUEST_METHOD'] = 'GET' + + t2 = thread_two(MockHandler(self.request_two), env, start_response) + t3 = thread_three(MockHandler(self.request_three), env, start_response) + + try: + self.ev.set() + MockHandler(self.request_one)(env, start_response) + finally: + t2.join() + t3.join() + err = self.thread_errors() + if err: + import traceback + for e in err: + traceback.print_exception(*e) + raise AssertionError("%s thread%s failed" % + (len(err), len(err) > 1 and 's' or + '')) + diff --git a/tests/regressiontests/urlpatterns_reverse/__init__.py b/tests/regressiontests/urlpatterns_reverse/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/urlpatterns_reverse/models.py b/tests/regressiontests/urlpatterns_reverse/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/othertests/urlpatterns_reverse.py b/tests/regressiontests/urlpatterns_reverse/tests.py similarity index 70% rename from tests/othertests/urlpatterns_reverse.py rename to tests/regressiontests/urlpatterns_reverse/tests.py index 236944d49f..8f571ac66c 100644 --- a/tests/othertests/urlpatterns_reverse.py +++ b/tests/regressiontests/urlpatterns_reverse/tests.py @@ -1,7 +1,7 @@ "Unit tests for reverse URL lookup" from django.core.urlresolvers import reverse_helper, NoReverseMatch -import re +import re, unittest test_data = ( ('^places/(\d+)/$', 'places/3/', [3], {}), @@ -25,23 +25,15 @@ test_data = ( ('^people/(?P\w\w)/(\w+)/$', 'people/il/adrian/', ['adrian'], {'state': 'il'}), ) -def run_tests(verbosity=0): - for regex, expected, args, kwargs in test_data: - passed = True - try: - got = reverse_helper(re.compile(regex), *args, **kwargs) - except NoReverseMatch, e: - if expected != NoReverseMatch: - passed, got = False, str(e) - else: - if got != expected: - passed, got = False, got - if passed and verbosity: - print "Passed: %s" % regex - elif not passed: - print "REVERSE LOOKUP FAILED: %s" % regex - print " Got: %s" % got - print " Expected: %r" % expected +class URLPatternReverse(unittest.TestCase): + def test_urlpattern_reverse(self): + for regex, expected, args, kwargs in test_data: + try: + got = reverse_helper(re.compile(regex), *args, **kwargs) + except NoReverseMatch, e: + self.assertEqual(expected, NoReverseMatch) + else: + self.assertEquals(got, expected) if __name__ == "__main__": run_tests(1) diff --git a/tests/runtests.py b/tests/runtests.py index 165636cbf4..a8dd08e898 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -1,31 +1,21 @@ #!/usr/bin/env python -import os, re, sys, time, traceback - -# doctest is included in the same package as this module, because this testing -# framework uses features only available in the Python 2.4 version of doctest, -# and Django aims to work with Python 2.3+. -import doctest +import os, sys, traceback +import unittest MODEL_TESTS_DIR_NAME = 'modeltests' -OTHER_TESTS_DIR = "othertests" REGRESSION_TESTS_DIR_NAME = 'regressiontests' TEST_DATABASE_NAME = 'django_test_db' -TEST_DATABASES = (TEST_DATABASE_NAME + '_a', TEST_DATABASE_NAME + '_b') + +TEST_DATABASES = ('_a', '_b') TEST_DATABASE_MODELS = { - TEST_DATABASE_NAME + '_a': [ 'multiple_databases.Artist', - 'multiple_databases.Opus' ], - TEST_DATABASE_NAME + '_b': [ 'multiple_databases.Widget', - 'multiple_databases.DooHickey' ] + '_a': [ 'multiple_databases.Artist', + 'multiple_databases.Opus' ], + '_b': [ 'multiple_databases.Widget', + 'multiple_databases.DooHickey' ] } -error_list = [] -def log_error(model_name, title, description): - error_list.append({ - 'title': "%r module: %s" % (model_name, title), - 'description': description, - }) MODEL_TEST_DIR = os.path.join(os.path.dirname(__file__), MODEL_TESTS_DIR_NAME) REGRESSION_TEST_DIR = os.path.join(os.path.dirname(__file__), REGRESSION_TESTS_DIR_NAME) @@ -45,333 +35,107 @@ def get_test_models(): models = [] for loc, dirpath in (MODEL_TESTS_DIR_NAME, MODEL_TEST_DIR), (REGRESSION_TESTS_DIR_NAME, REGRESSION_TEST_DIR): for f in os.listdir(dirpath): - if f.startswith('__init__') or f.startswith('.') or f.startswith('sql'): + if f.startswith('__init__') or f.startswith('.') or f.startswith('sql') or f.startswith('invalid'): continue models.append((loc, f)) return models -class DjangoDoctestRunner(doctest.DocTestRunner): - def __init__(self, verbosity_level, *args, **kwargs): - self.verbosity_level = verbosity_level - doctest.DocTestRunner.__init__(self, *args, **kwargs) - self._checker = DjangoDoctestOutputChecker() - self.optionflags = doctest.ELLIPSIS - - def report_start(self, out, test, example): - if self.verbosity_level > 1: - out(" >>> %s\n" % example.source.strip()) - - def report_failure(self, out, test, example, got): - log_error(test.name, "API test failed", - "Code: %r\nLine: %s\nExpected: %r\nGot: %r" % (example.source.strip(), example.lineno, example.want, got)) - - def report_unexpected_exception(self, out, test, example, exc_info): - from django.db import transaction - tb = ''.join(traceback.format_exception(*exc_info)[1:]) - log_error(test.name, "API test raised an exception", - "Code: %r\nLine: %s\nException: %s" % (example.source.strip(), example.lineno, tb)) - # Rollback, in case of database errors. Otherwise they'd have - # side effects on other tests. - transaction.rollback_unless_managed() - -normalize_long_ints = lambda s: re.sub(r'(? required_level - 1: - print message - - def create_test_db(self, db_name, connection): - """Create a test db, returning a ConnectionInfo object holding - a connection to that db. - """ - from django.db import connect - - # settings may be a dict or settings object - settings = connection.settings - - if settings.DATABASE_ENGINE != "sqlite3": - # Create the test database and connect to it. We need to autocommit - # if the database supports it because PostgreSQL doesn't allow - # CREATE/DROP DATABASE statements within transactions. - cursor = connection.cursor() - self._set_autocommit(connection) - self.output(1, "Creating test database %s for %s" % - (db_name, settings.DATABASE_NAME)) - try: - cursor.execute("CREATE DATABASE %s" % db_name) - except Exception, e: - sys.stderr.write("Got an error creating the test database: %s\n" % e) - confirm = raw_input("It appears the test database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % db_name) - if confirm == 'yes': - cursor.execute("DROP DATABASE %s" % db_name) - cursor.execute("CREATE DATABASE %s" % db_name) - else: - raise Exception("Tests cancelled.") - - - settings.DATABASE_NAME = db_name - connection.close() - - # Initialize the test database. - info = connect(settings) - cursor = info.connection.cursor() - self.created_dbs.append((db_name, info)) - return info - - def run_tests(self): - self.setup() - try: - self._run_tests() - finally: - self.teardown() - - def setup(self): - global TEST_DATABASE_NAME - from django.conf import settings - from django.db import connection, connections - from django.core import management - from django.db.models.loading import load_app - - # An empty access of the settings to force the default options to be - # installed prior to assigning to them. - settings.INSTALLED_APPS - - # Manually set INSTALLED_APPS to point to the test models. - settings.INSTALLED_APPS = ALWAYS_INSTALLED_APPS + ['.'.join(a) for a in get_test_models()] - - # Manually set DEBUG and USE_I18N. - settings.DEBUG = False - settings.USE_I18N = True - - self.output(0, "Running tests with database %r" % settings.DATABASE_ENGINE) - - # Create test dbs for the default connection and two named connections, - # replacing any named connections defined in settings. All connections - # will use the default DATABASE_ENGINE - self.old_database_name = settings.DATABASE_NAME - self.old_databases = settings.OTHER_DATABASES - - if settings.DATABASE_ENGINE == 'sqlite3': - # If we're using SQLite, it's more convenient to test against an - # in-memory database. But we can only do this for the default; - # after that we have to use temp files. - TEST_DATABASE_NAME = ':memory:' - - new_databases = {} - for db_name in TEST_DATABASES: - db_st = settings.OTHER_DATABASES.get(db_name, {}) - engine = db_st.get('DATABASE_ENGINE', settings.DATABASE_ENGINE) - if engine == 'sqlite3': - db_st['DATABASE_NAME'] = self._tempfile() - self.cleanup_files.append(db_st['DATABASE_NAME']) - else: - db_st['DATABASE_NAME'] = db_name - if db_name in TEST_DATABASE_MODELS: - db_st['MODELS'] = TEST_DATABASE_MODELS[db_name] - new_databases[db_name] = db_st - settings.OTHER_DATABASES = new_databases - - self.create_test_db(TEST_DATABASE_NAME, connection) - for name, info in settings.OTHER_DATABASES.items(): - cx = connections[name] - test_connection = self.create_test_db(info['DATABASE_NAME'], - cx.connection) - connections[name] = test_connection - - # Install the core always installed apps - for app in ALWAYS_INSTALLED_APPS: - self.output(1, "Installing contrib app %s" % app) - mod = load_app(app) - management.install(mod) - - def teardown(self): - # Unless we're using SQLite, remove the test database to clean up after - # ourselves. Connect to the previous database (not the test database) - # to do so, because it's not allowed to delete a database while being - # connected to it. - from django.db import connection - from django.conf import settings - connection.close() - settings.DATABASE_NAME = self.old_database_name - settings.OTHER_DATABASES = self.old_databases - for db_name, cx in self.created_dbs: - settings = cx.settings - cx.close() - if settings.DATABASE_ENGINE != "sqlite3": - cursor = connection.cursor() - self.output(1, "Deleting test database %s" % db_name) - self._set_autocommit(connection) - time.sleep(1) # To avoid "database is being accessed by other users" errors. - cursor.execute("DROP DATABASE %s" % db_name) - - # Clean up sqlite dbs created on the filesystem - for filename in self.cleanup_files: - if os.path.exists(filename): - os.unlink(filename) - - def _run_tests(self): - # Run the tests for each test model. - from django.core import management - from django.db.models.loading import load_app - from django.db import models - - # Determine which models we're going to test. - test_models = get_test_models() - if 'othertests' in self.which_tests: - self.which_tests.remove('othertests') - run_othertests = True - if not self.which_tests: - test_models = [] - else: - run_othertests = not self.which_tests - - if self.which_tests: - # Only run the specified tests. - bad_models = [m for m in self.which_tests if (MODEL_TESTS_DIR_NAME, m) not in test_models and (REGRESSION_TESTS_DIR_NAME, m) not in test_models] - if bad_models: - sys.stderr.write("Models not found: %s\n" % bad_models) - sys.exit(1) - else: - all_tests = [] - for test in self.which_tests: - for loc in MODEL_TESTS_DIR_NAME, REGRESSION_TESTS_DIR_NAME: - if (loc, test) in test_models: - all_tests.append((loc, test)) - test_models = all_tests - - self.output(1, "Running app tests") - for model_dir, model_name in test_models: - self.output(1, "%s model: Importing" % model_name) - try: - mod = load_app(model_dir + '.' + model_name) - except Exception, e: - log_error(model_name, "Error while importing", ''.join(traceback.format_exception(*sys.exc_info())[1:])) +def get_invalid_models(): + models = [] + for loc, dirpath in (MODEL_TESTS_DIR_NAME, MODEL_TEST_DIR), (REGRESSION_TESTS_DIR_NAME, REGRESSION_TEST_DIR): + for f in os.listdir(dirpath): + if f.startswith('__init__') or f.startswith('.') or f.startswith('sql'): continue + if f.startswith('invalid'): + models.append((loc, f)) + return models + +class InvalidModelTestCase(unittest.TestCase): + def __init__(self, model_label): + unittest.TestCase.__init__(self) + self.model_label = model_label + + def runTest(self): + from django.core import management + from django.db.models.loading import load_app + from cStringIO import StringIO - if not getattr(mod, 'error_log', None): - # Model is not marked as an invalid model - self.output(1, "%s.%s model: Installing" % (model_dir, model_name)) - management.install(mod) + try: + module = load_app(self.model_label) + except Exception, e: + self.fail('Unable to load invalid model module') + + s = StringIO() + count = management.get_validation_errors(s, module) + s.seek(0) + error_log = s.read() + actual = error_log.split('\n') + expected = module.model_errors.split('\n') - # Run the API tests. - p = doctest.DocTestParser() - test_namespace = dict([(m._meta.object_name, m) \ - for m in models.get_models(mod)]) - dtest = p.get_doctest(mod.API_TESTS, test_namespace, model_name, None, None) - # Manually set verbose=False, because "-v" command-line parameter - # has side effects on doctest TestRunner class. - runner = DjangoDoctestRunner(verbosity_level=verbosity_level, verbose=False) - self.output(1, "%s.%s model: Running tests" % (model_dir, model_name)) - runner.run(dtest, clear_globs=True, out=sys.stdout.write) - else: - # Check that model known to be invalid is invalid for the right reasons. - self.output(1, "%s.%s model: Validating" % (model_dir, model_name)) + unexpected = [err for err in actual if err not in expected] + missing = [err for err in expected if err not in actual] - from cStringIO import StringIO - s = StringIO() - count = management.get_validation_errors(s, mod) - s.seek(0) - error_log = s.read() - actual = error_log.split('\n') - expected = mod.error_log.split('\n') + self.assert_(not unexpected, "Unexpected Errors: " + '\n'.join(unexpected)) + self.assert_(not missing, "Missing Errors: " + '\n'.join(missing)) - unexpected = [err for err in actual if err not in expected] - missing = [err for err in expected if err not in actual] +def django_tests(verbosity, tests_to_run): + from django.conf import settings + from django.db.models.loading import get_apps, load_app + old_installed_apps = settings.INSTALLED_APPS + old_test_database_name = settings.TEST_DATABASE_NAME + + settings.TEST_DATABASE_NAME = TEST_DATABASE_NAME + settings.INSTALLED_APPS = ALWAYS_INSTALLED_APPS - if unexpected or missing: - unexpected_log = '\n'.join(unexpected) - missing_log = '\n'.join(missing) - log_error(model_name, - "Validator found %d validation errors, %d expected" % (count, len(expected) - 1), - "Missing errors:\n%s\n\nUnexpected errors:\n%s" % (missing_log, unexpected_log)) - - if run_othertests: - # Run the non-model tests in the other tests dir - self.output(1, "Running other tests") - other_tests_dir = os.path.join(os.path.dirname(__file__), OTHER_TESTS_DIR) - test_modules = [f[:-3] for f in os.listdir(other_tests_dir) if f.endswith('.py') and not f.startswith('__init__')] - for module in test_modules: - self.output(1, "%s module: Importing" % module) - try: - mod = __import__("othertests." + module, '', '', ['']) - except Exception, e: - log_error(module, "Error while importing", ''.join(traceback.format_exception(*sys.exc_info())[1:])) - continue - if mod.__doc__: - p = doctest.DocTestParser() - dtest = p.get_doctest(mod.__doc__, mod.__dict__, module, None, None) - runner = DjangoDoctestRunner(verbosity_level=verbosity_level, verbose=False) - self.output(1, "%s module: running tests" % module) - runner.run(dtest, clear_globs=True, out=sys.stdout.write) - if hasattr(mod, "run_tests") and callable(mod.run_tests): - self.output(1, "%s module: running tests" % module) - try: - mod.run_tests(verbosity_level) - except Exception, e: - log_error(module, "Exception running tests", ''.join(traceback.format_exception(*sys.exc_info())[1:])) - continue - - # Display output. - if error_list: - for d in error_list: - print - print d['title'] - print "=" * len(d['title']) - print d['description'] - print "%s error%s:" % (len(error_list), len(error_list) != 1 and 's' or '') - else: - print "All tests passed." - - def _set_autocommit(self, connection): - """ - Make sure a connection is in autocommit mode. - """ - if hasattr(connection.connection, "autocommit"): - connection.connection.autocommit(True) - elif hasattr(connection.connection, "set_isolation_level"): - connection.connection.set_isolation_level(0) - - def _tempfile(self): - import tempfile - fd, filename = tempfile.mkstemp() - os.close(fd) - return filename + settings.TEST_DATABASES = TEST_DATABASES + settings.TEST_DATABASE_MODELS = TEST_DATABASE_MODELS + + # load all the ALWAYS_INSTALLED_APPS + get_apps() + + test_models = [] + # Load all the test model apps + for model_dir, model_name in get_test_models(): + model_label = '.'.join([model_dir, model_name]) + try: + # if the model was named on the command line, or + # no models were named (i.e., run all), import + # this model and add it to the list to test. + if not tests_to_run or model_name in tests_to_run: + if verbosity >= 1: + print "Importing model %s" % model_name + mod = load_app(model_label) + settings.INSTALLED_APPS.append(model_label) + test_models.append(mod) + except Exception, e: + sys.stderr.write("Error while importing %s:" % model_name + ''.join(traceback.format_exception(*sys.exc_info())[1:])) + continue + # Add tests for invalid models + extra_tests = [] + for model_dir, model_name in get_invalid_models(): + model_label = '.'.join([model_dir, model_name]) + if not tests_to_run or model_name in tests_to_run: + extra_tests.append(InvalidModelTestCase(model_label)) + + # Run the test suite, including the extra validation tests. + from django.test.simple import run_tests + run_tests(test_models, verbosity, extra_tests=extra_tests) + + # Restore the old settings + settings.INSTALLED_APPS = old_installed_apps + settings.TESTS_DATABASE_NAME = old_test_database_name if __name__ == "__main__": from optparse import OptionParser usage = "%prog [options] [model model model ...]" parser = OptionParser(usage=usage) - parser.add_option('-v', help='How verbose should the output be? Choices are 0, 1 and 2, where 2 is most verbose. Default is 0.', - type='choice', choices=['0', '1', '2']) + parser.add_option('-v','--verbosity', action='store', dest='verbosity', default='0', + type='choice', choices=['0', '1', '2'], + help='Verbosity level; 0=minimal output, 1=normal output, 2=all output') parser.add_option('--settings', help='Python path to settings module, e.g. "myproject.settings". If this isn\'t provided, the DJANGO_SETTINGS_MODULE environment variable will be used.') options, args = parser.parse_args() - verbosity_level = 0 - if options.v: - verbosity_level = int(options.v) if options.settings: os.environ['DJANGO_SETTINGS_MODULE'] = options.settings - t = TestRunner(verbosity_level, args) - t.run_tests() + + django_tests(int(options.verbosity), args)