Ticket #7596: t7596-alex.diff.txt

File t7596-alex.diff.txt, 24.5 KB (added by Russell Keith-Magee, 13 years ago)

Alex Gaynor's patch, prepared at DjangoCon Europe 2011

Line 
1diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py
2index 9966849..876d84d 100644
3--- a/django/contrib/auth/management/__init__.py
4+++ b/django/contrib/auth/management/__init__.py
5@@ -46,16 +46,14 @@ def create_permissions(app, created_models, verbosity, **kwargs):
6 "content_type", "codename"
7 ))
8
9- for ctype, (codename, name) in searched_perms:
10- # If the permissions exists, move on.
11- if (ctype.pk, codename) in all_perms:
12- continue
13- p = auth_app.Permission.objects.create(
14- codename=codename,
15- name=name,
16- content_type=ctype
17- )
18- if verbosity >= 2:
19+ objs = [
20+ auth_app.Permission(codename=codename, name=name, content_type=ctype)
21+ for ctype, (codename, name) in searched_perms
22+ if (ctype.pk, codename) not in all_perms
23+ ]
24+ auth_app.Permission.objects.bulk_create(objs)
25+ if verbosity >= 2:
26+ for obj in objs:
27 print "Adding permission '%s'" % p
28
29
30diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
31index b64fb01..8ed6d6b 100644
32--- a/django/db/backends/__init__.py
33+++ b/django/db/backends/__init__.py
34@@ -272,8 +272,10 @@ class BaseDatabaseFeatures(object):
35
36 can_use_chunked_reads = True
37 can_return_id_from_insert = False
38+ has_bulk_insert = False
39 uses_autocommit = False
40 uses_savepoints = False
41+ can_combine_inserts_with_and_without_auto_increment_pk = False
42
43 # If True, don't use integer foreign keys referring to, e.g., positive
44 # integer primary keys.
45diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py
46index 67e2877..ad355b8 100644
47--- a/django/db/backends/postgresql_psycopg2/base.py
48+++ b/django/db/backends/postgresql_psycopg2/base.py
49@@ -72,6 +72,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
50 can_defer_constraint_checks = True
51 has_select_for_update = True
52 has_select_for_update_nowait = True
53+ has_bulk_insert = True
54
55
56 class DatabaseWrapper(BaseDatabaseWrapper):
57diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py
58index 3315913..7a9c406 100644
59--- a/django/db/backends/postgresql_psycopg2/operations.py
60+++ b/django/db/backends/postgresql_psycopg2/operations.py
61@@ -208,3 +208,7 @@ class DatabaseOperations(BaseDatabaseOperations):
62
63 def return_insert_id(self):
64 return "RETURNING %s", ()
65+
66+ def bulk_insert_sql(self, fields, num_values):
67+ items_sql = "(%s)" % ", ".join(["%s"] * len(fields))
68+ return "VALUES " + ", ".join([items_sql] * num_values)
69\ No newline at end of file
70diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
71index 5b4a1c2..ea65865 100644
72--- a/django/db/backends/sqlite3/base.py
73+++ b/django/db/backends/sqlite3/base.py
74@@ -57,6 +57,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
75 supports_unspecified_pk = True
76 supports_1000_query_parameters = False
77 supports_mixed_date_datetime_comparisons = False
78+ has_bulk_insert = True
79+ can_combine_inserts_with_and_without_auto_increment_pk = True
80
81 def _supports_stddev(self):
82 """Confirm support for STDDEV and related stats functions
83@@ -105,7 +107,7 @@ class DatabaseOperations(BaseDatabaseOperations):
84 return ""
85
86 def pk_default_value(self):
87- return 'NULL'
88+ return "NULL"
89
90 def quote_name(self, name):
91 if name.startswith('"') and name.endswith('"'):
92@@ -153,6 +155,14 @@ class DatabaseOperations(BaseDatabaseOperations):
93 # No field, or the field isn't known to be a decimal or integer
94 return value
95
96+ def bulk_insert_sql(self, fields, num_values):
97+ res = []
98+ res.append("SELECT %s" % ", ".join(
99+ "%%s AS %s" % self.quote_name(f.column) for f in fields
100+ ))
101+ res.extend(["UNION SELECT %s" % ", ".join(["%s"] * len(fields))] * (num_values - 1))
102+ return " ".join(res)
103+
104 class DatabaseWrapper(BaseDatabaseWrapper):
105 vendor = 'sqlite'
106 # SQLite requires LIKE statements to include an ESCAPE clause if the value
107diff --git a/django/db/models/base.py b/django/db/models/base.py
108index 31310ea..f71f13e 100644
109--- a/django/db/models/base.py
110+++ b/django/db/models/base.py
111@@ -539,24 +539,16 @@ class Model(object):
112 order_value = manager.using(using).filter(**{field.name: getattr(self, field.attname)}).count()
113 self._order = order_value
114
115+ fields = meta.local_fields
116 if not pk_set:
117 if force_update:
118 raise ValueError("Cannot force an update in save() with no primary key.")
119- values = [(f, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True), connection=connection))
120- for f in meta.local_fields if not isinstance(f, AutoField)]
121- else:
122- values = [(f, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True), connection=connection))
123- for f in meta.local_fields]
124+ fields = [f for f in fields if not isinstance(f, AutoField)]
125
126 record_exists = False
127
128 update_pk = bool(meta.has_auto_field and not pk_set)
129- if values:
130- # Create a new record.
131- result = manager._insert(values, return_id=update_pk, using=using)
132- else:
133- # Create a new record with defaults for everything.
134- result = manager._insert([(meta.pk, connection.ops.pk_default_value())], return_id=update_pk, raw_values=True, using=using)
135+ result = manager._insert([self], fields=fields, return_id=update_pk, using=using, raw=raw)
136
137 if update_pk:
138 setattr(self, meta.pk.attname, result)
139diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
140index cedf308..4d6a8a7 100644
141--- a/django/db/models/fields/related.py
142+++ b/django/db/models/fields/related.py
143@@ -432,7 +432,7 @@ class ForeignRelatedObjectsDescriptor(object):
144 add.alters_data = True
145
146 def create(self, **kwargs):
147- kwargs.update({rel_field.name: instance})
148+ kwargs[rel_field.name] = instance
149 db = router.db_for_write(rel_model, instance=instance)
150 return super(RelatedManager, self.db_manager(db)).create(**kwargs)
151 create.alters_data = True
152@@ -440,7 +440,7 @@ class ForeignRelatedObjectsDescriptor(object):
153 def get_or_create(self, **kwargs):
154 # Update kwargs with the related object that this
155 # ForeignRelatedObjectsDescriptor knows about.
156- kwargs.update({rel_field.name: instance})
157+ kwargs[rel_field.name] = instance
158 db = router.db_for_write(rel_model, instance=instance)
159 return super(RelatedManager, self.db_manager(db)).get_or_create(**kwargs)
160 get_or_create.alters_data = True
161@@ -580,11 +580,13 @@ def create_many_related_manager(superclass, rel=False):
162 instance=self.instance, reverse=self.reverse,
163 model=self.model, pk_set=new_ids, using=db)
164 # Add the ones that aren't there already
165- for obj_id in new_ids:
166- self.through._default_manager.using(db).create(**{
167+ self.through._default_manager.using(db).bulk_create([
168+ self.through(**{
169 '%s_id' % source_field_name: self._pk_val,
170 '%s_id' % target_field_name: obj_id,
171 })
172+ for obj_id in new_ids
173+ ])
174 if self.reverse or source_field_name == self.source_field_name:
175 # Don't send the signal when we are inserting the
176 # duplicate data row for symmetrical reverse entries.
177@@ -703,12 +705,12 @@ class ReverseManyRelatedObjectsDescriptor(object):
178 def __init__(self, m2m_field):
179 self.field = m2m_field
180
181- def _through(self):
182+ @property
183+ def through(self):
184 # through is provided so that you have easy access to the through
185 # model (Book.authors.through) for inlines, etc. This is done as
186 # a property to ensure that the fully resolved value is returned.
187 return self.field.rel.through
188- through = property(_through)
189
190 def __get__(self, instance, instance_type=None):
191 if instance is None:
192diff --git a/django/db/models/manager.py b/django/db/models/manager.py
193index 4fa4c4a..92090e0 100644
194--- a/django/db/models/manager.py
195+++ b/django/db/models/manager.py
196@@ -137,6 +137,9 @@ class Manager(object):
197 def create(self, **kwargs):
198 return self.get_query_set().create(**kwargs)
199
200+ def bulk_create(self, *args, **kwargs):
201+ return self.get_query_set().bulk_create(*args, **kwargs)
202+
203 def filter(self, *args, **kwargs):
204 return self.get_query_set().filter(*args, **kwargs)
205
206@@ -194,8 +197,8 @@ class Manager(object):
207 def exists(self, *args, **kwargs):
208 return self.get_query_set().exists(*args, **kwargs)
209
210- def _insert(self, values, **kwargs):
211- return insert_query(self.model, values, **kwargs)
212+ def _insert(self, objs, fields, **kwargs):
213+ return insert_query(self.model, objs, fields, **kwargs)
214
215 def _update(self, values, **kwargs):
216 return self.get_query_set()._update(values, **kwargs)
217diff --git a/django/db/models/query.py b/django/db/models/query.py
218index 6a6a829..86060cd 100644
219--- a/django/db/models/query.py
220+++ b/django/db/models/query.py
221@@ -7,7 +7,7 @@ from itertools import izip
222
223 from django.db import connections, router, transaction, IntegrityError
224 from django.db.models.aggregates import Aggregate
225-from django.db.models.fields import DateField
226+from django.db.models.fields import DateField, AutoField
227 from django.db.models.query_utils import (Q, select_related_descend,
228 deferred_class_factory, InvalidQuery)
229 from django.db.models.deletion import Collector
230@@ -360,6 +360,39 @@ class QuerySet(object):
231 obj.save(force_insert=True, using=self.db)
232 return obj
233
234+ def bulk_create(self, objs):
235+ """
236+ Inserts each of the instances into the database. This does *not* call
237+ save() on each of the instances, does not send any pre/post save
238+ signals, and does not set the primary key attribute if it is an
239+ autoincrement field.
240+ """
241+ # So this case is fun. When you bulk insert you don't get the primary
242+ # keys back (if it's an autoincrement), so you can't insert into the
243+ # child tables which references this. There are two workarounds, 1)
244+ # this could be implemented if you didn't have an autoincrement pk,
245+ # and 2) you could do it by doing O(n) normal inserts into the parent
246+ # tables to get the primary keys back, and then doing a single bulk
247+ # insert into the childmost table. We're punting on these for now
248+ # because they are relatively rare cases.
249+ if self.model._meta.parents:
250+ raise ValueError("Can't bulk create an inherited model")
251+ if not objs:
252+ return
253+ self._for_write = True
254+ connection = connections[self.db]
255+ fields = self.model._meta.local_fields
256+ if (connection.features.can_combine_inserts_with_and_without_auto_increment_pk
257+ and self.model._meta.has_auto_field):
258+ self.model._base_manager._insert(objs, fields=fields, using=self.db)
259+ else:
260+ objs_with_pk = [o for o in objs if o.pk]
261+ objs_without_pk = [o for o in objs if not o.pk]
262+ if objs_with_pk:
263+ self.model._base_manager._insert(objs_with_pk, fields=fields, using=self.db)
264+ if objs_without_pk:
265+ self.model._base_manager._insert(objs_without_pk, fields=[f for f in fields if not isinstance(f, AutoField)], using=self.db)
266+
267 def get_or_create(self, **kwargs):
268 """
269 Looks up an object with the given kwargs, creating one if necessary.
270@@ -1445,12 +1478,12 @@ class RawQuerySet(object):
271 self._model_fields[converter(column)] = field
272 return self._model_fields
273
274-def insert_query(model, values, return_id=False, raw_values=False, using=None):
275+def insert_query(model, objs, fields, return_id=False, raw=False, using=None):
276 """
277 Inserts a new record for the given model. This provides an interface to
278 the InsertQuery class and is how Model.save() is implemented. It is not
279 part of the public API.
280 """
281 query = sql.InsertQuery(model)
282- query.insert_values(values, raw_values)
283+ query.insert_values(fields, objs, raw=raw)
284 return query.get_compiler(using=using).execute_sql(return_id)
285diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py
286index 841ec12..84dd7ed 100644
287--- a/django/db/models/sql/compiler.py
288+++ b/django/db/models/sql/compiler.py
289@@ -1,3 +1,5 @@
290+from itertools import izip
291+
292 from django.core.exceptions import FieldError
293 from django.db import connections
294 from django.db import transaction
295@@ -9,6 +11,7 @@ from django.db.models.sql.query import get_proxied_model, get_order_dir, \
296 select_related_descend, Query
297 from django.db.utils import DatabaseError
298
299+
300 class SQLCompiler(object):
301 def __init__(self, query, connection, using):
302 self.query = query
303@@ -794,20 +797,55 @@ class SQLInsertCompiler(SQLCompiler):
304 qn = self.connection.ops.quote_name
305 opts = self.query.model._meta
306 result = ['INSERT INTO %s' % qn(opts.db_table)]
307- result.append('(%s)' % ', '.join([qn(c) for c in self.query.columns]))
308- values = [self.placeholder(*v) for v in self.query.values]
309- result.append('VALUES (%s)' % ', '.join(values))
310- params = self.query.params
311+
312+ has_fields = bool(self.query.fields)
313+ fields = self.query.fields if has_fields else [opts.pk]
314+ result.append('(%s)' % ', '.join([qn(f.column) for f in fields]))
315+
316+ if has_fields:
317+ params = values = [
318+ [
319+ f.get_db_prep_save(getattr(obj, f.attname) if self.query.raw else f.pre_save(obj, True), connection=self.connection)
320+ for f in fields
321+ ]
322+ for obj in self.query.objs
323+ ]
324+ else:
325+ values = [[self.connection.ops.pk_default_value()] for obj in self.query.objs]
326+ params = [[]]
327+ fields = [None]
328+ can_bulk = not any(hasattr(field, "get_placeholder") for field in fields) and not self.return_id
329+
330+ if can_bulk:
331+ placeholders = [["%s"] * len(fields)]
332+ else:
333+ placeholders = [
334+ [self.placeholder(field, v) for field, v in izip(fields, val)]
335+ for val in values
336+ ]
337 if self.return_id and self.connection.features.can_return_id_from_insert:
338- col = "%s.%s" % (qn(opts.db_table), qn(opts.pk.column))
339+ params = values[0]
340+ col = "%s.%s" % (qn(opst.db_table), qn(opts.pk.column))
341+ result.append("VALUES (%s)" % ", ".join(placeholders[0]))
342 r_fmt, r_params = self.connection.ops.return_insert_id()
343 result.append(r_fmt % col)
344- params = params + r_params
345- return ' '.join(result), params
346+ params += r_params
347+ return [(" ".join(result), tuple(param))]
348+ if can_bulk and self.connection.features.has_bulk_insert:
349+ result.append(self.connection.ops.bulk_insert_sql(fields, len(values)))
350+ return [(" ".join(result), tuple([v for val in values for v in val]))]
351+ else:
352+ return [
353+ (" ".join(result + ["VALUES (%s)" % ", ".join(p)]), vals)
354+ for p, vals in izip(placeholders, params)
355+ ]
356
357 def execute_sql(self, return_id=False):
358+ assert not (return_id and len(self.query.objs) != 1)
359 self.return_id = return_id
360- cursor = super(SQLInsertCompiler, self).execute_sql(None)
361+ cursor = self.connection.cursor()
362+ for sql, params in self.as_sql():
363+ cursor.execute(sql, params)
364 if not (return_id and cursor):
365 return
366 if self.connection.features.can_return_id_from_insert:
367diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
368index 99663b6..12b13a7 100644
369--- a/django/db/models/sql/query.py
370+++ b/django/db/models/sql/query.py
371@@ -8,9 +8,10 @@ all about the internals of models in order to get the information it needs.
372 """
373
374 import copy
375-from django.utils.tree import Node
376+
377 from django.utils.datastructures import SortedDict
378 from django.utils.encoding import force_unicode
379+from django.utils.tree import Node
380 from django.db import connections, DEFAULT_DB_ALIAS
381 from django.db.models import signals
382 from django.db.models.fields import FieldDoesNotExist
383diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py
384index 003bf43..39cfc03 100644
385--- a/django/db/models/sql/subqueries.py
386+++ b/django/db/models/sql/subqueries.py
387@@ -138,20 +138,19 @@ class InsertQuery(Query):
388
389 def __init__(self, *args, **kwargs):
390 super(InsertQuery, self).__init__(*args, **kwargs)
391- self.columns = []
392- self.values = []
393- self.params = ()
394+ self.fields = []
395+ self.objs = []
396
397 def clone(self, klass=None, **kwargs):
398 extras = {
399- 'columns': self.columns[:],
400- 'values': self.values[:],
401- 'params': self.params
402+ 'fields': self.fields[:],
403+ 'objs': self.objs[:],
404+ 'raw': self.raw,
405 }
406 extras.update(kwargs)
407 return super(InsertQuery, self).clone(klass, **extras)
408
409- def insert_values(self, insert_values, raw_values=False):
410+ def insert_values(self, fields, objs, raw=False):
411 """
412 Set up the insert query from the 'insert_values' dictionary. The
413 dictionary gives the model field names and their target values.
414@@ -161,16 +160,9 @@ class InsertQuery(Query):
415 parameters. This provides a way to insert NULL and DEFAULT keywords
416 into the query, for example.
417 """
418- placeholders, values = [], []
419- for field, val in insert_values:
420- placeholders.append((field, val))
421- self.columns.append(field.column)
422- values.append(val)
423- if raw_values:
424- self.values.extend([(None, v) for v in values])
425- else:
426- self.params += tuple(values)
427- self.values.extend(placeholders)
428+ self.fields = fields
429+ self.objs = objs
430+ self.raw = raw
431
432 class DateQuery(Query):
433 """
434diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt
435index 2bd813d..b7a84cd 100644
436--- a/docs/ref/models/querysets.txt
437+++ b/docs/ref/models/querysets.txt
438@@ -139,7 +139,7 @@ Though you usually won't create one manually -- you'll go through a
439 clause or a default ordering on the model. ``False`` otherwise.
440
441 .. attribute:: db
442-
443+
444 The database that will be used if this query is executed now.
445
446 .. note::
447@@ -1139,6 +1139,29 @@ has a side effect on your data. For more, see `Safe methods`_ in the HTTP spec.
448
449 .. _Safe methods: http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1
450
451+bulk_create
452+~~~~~~~~~~~
453+
454+.. method:: bulk_create(objs)
455+
456+This method inserts the provided list of objects into the database in an
457+efficient manner (generally only 1 query, no matter how many objects there
458+are)::
459+
460+ >>> Entry.objects.bulk_create([
461+ ... Entry(headline="Django 1.0 Released"),
462+ ... Entry(headline="Django 1.1 Announced"),
463+ ... Entry(headline="Breaking: Django is awesome")
464+ ... ])
465+
466+This has a number of caveats though:
467+
468+ * The model's ``save()`` method will not be called, and the ``pre_save`` and
469+ ``post_save`` signals will not be sent.
470+ * It does not work with child models in a multi-table inheritance scenario.
471+ * If the model's primary key is an :class:`AutoField` it does not retrieve
472+ and set the primary key attribute, as ``save()`` does.
473+
474 count
475 ~~~~~
476
477diff --git a/docs/topics/db/optimization.txt b/docs/topics/db/optimization.txt
478index 265ef55..24830aa 100644
479--- a/docs/topics/db/optimization.txt
480+++ b/docs/topics/db/optimization.txt
481@@ -268,3 +268,29 @@ instead of::
482
483 entry.blog.id
484
485+Insert in bulk
486+==============
487+
488+When creating objects, where possible, use the :meth:`QuerySet.bulk_create()`
489+method to reduce the number of SQL queries. For example::
490+
491+ Entry.objects.bulk_create([
492+ Entry(headline="Python 3.0 Released"),
493+ Entry(headline="Python 3.1 Planned")
494+ ])
495+
496+Is preferable to::
497+
498+ Entry.objects.create(headline="Python 3.0 Released")
499+ Entry.objects.create(headline="Python 3.1 Planned")
500+
501+This also applies to :class:`ManyToManyFields`, doing::
502+
503+ my_band.members.add(me, my_friend)
504+
505+Is preferable to::
506+
507+ my_band.members.add(me)
508+ my_band.members.add(my_friend)
509+
510+Where ``Bands`` and ``Artists`` have a many-to-many relationship.
511\ No newline at end of file
512diff --git a/tests/regressiontests/bulk_create/models.py b/tests/regressiontests/bulk_create/models.py
513index 1f98a40..a4c611d 100644
514--- a/tests/regressiontests/bulk_create/models.py
515+++ b/tests/regressiontests/bulk_create/models.py
516@@ -3,4 +3,19 @@ from django.db import models
517
518 class Country(models.Model):
519 name = models.CharField(max_length=255)
520- iso_two_letter = models.CharField(max_length=2)
521\ No newline at end of file
522+ iso_two_letter = models.CharField(max_length=2)
523+
524+class Place(models.Model):
525+ name = models.CharField(max_length=100)
526+
527+ class Meta:
528+ abstract = True
529+
530+class Restaurant(Place):
531+ pass
532+
533+class Pizzeria(Restaurant):
534+ pass
535+
536+class State(models.Model):
537+ two_letter_code = models.CharField(max_length=2, primary_key=True)
538\ No newline at end of file
539diff --git a/tests/regressiontests/bulk_create/tests.py b/tests/regressiontests/bulk_create/tests.py
540index 42ba095..020841c 100644
541--- a/tests/regressiontests/bulk_create/tests.py
542+++ b/tests/regressiontests/bulk_create/tests.py
543@@ -1,8 +1,10 @@
544+from __future__ import with_statement
545+
546 from operator import attrgetter
547
548 from django.test import TestCase, skipUnlessDBFeature
549
550-from models import Country
551+from models import Country, Restaurant, Pizzeria, State
552
553
554 class BulkCreateTests(TestCase):
555@@ -23,4 +25,29 @@ class BulkCreateTests(TestCase):
556 @skipUnlessDBFeature("has_bulk_insert")
557 def test_efficiency(self):
558 with self.assertNumQueries(1):
559- Country.objects.bulk_create(self.data)
560\ No newline at end of file
561+ Country.objects.bulk_create(self.data)
562+
563+ def test_inheritance(self):
564+ Restaurant.objects.bulk_create([
565+ Restaurant(name="Nicholas's")
566+ ])
567+ self.assertQuerysetEqual(Restaurant.objects.all(), [
568+ "Nicholas's",
569+ ], attrgetter("name"))
570+ with self.assertRaises(ValueError):
571+ Pizzeria.objects.bulk_create([
572+ Pizzeria(name="The Art of Pizza")
573+ ])
574+ self.assertQuerysetEqual(Pizzeria.objects.all(), [])
575+ self.assertQuerysetEqual(Restaurant.objects.all(), [
576+ "Nicholas's",
577+ ], attrgetter("name"))
578+
579+ def test_non_auto_increment_pk(self):
580+ State.objects.bulk_create([
581+ State(two_letter_code=s)
582+ for s in ["IL", "NY", "CA", "ME"]
583+ ])
584+ self.assertQuerysetEqual(State.objects.order_by("two_letter_code"), [
585+ "CA", "IL", "ME", "NY",
586+ ], attrgetter("two_letter_code"))
587\ No newline at end of file
588diff --git a/tests/regressiontests/db_typecasts/tests.py b/tests/regressiontests/db_typecasts/tests.py
589index 8c71c8f..1d3bbfa 100644
590--- a/tests/regressiontests/db_typecasts/tests.py
591+++ b/tests/regressiontests/db_typecasts/tests.py
592@@ -53,10 +53,10 @@ TEST_CASES = {
593
594 class DBTypeCasts(unittest.TestCase):
595 def test_typeCasts(self):
596- for k, v in TEST_CASES.items():
597+ for k, v in TEST_CASES.iteritems():
598 for inpt, expected in v:
599 got = getattr(typecasts, k)(inpt)
600- assert got == expected, "In %s: %r doesn't match %r. Got %r instead." % (k, inpt, expected, got)
601+ self.assertEqual(got, expected, "In %s: %r doesn't match %r. Got %r instead." % (k, inpt, expected, got))
602
603 if __name__ == '__main__':
604 unittest.main()
Back to Top