Ticket #15648: namedtuples.patch
File namedtuples.patch, 11.2 KB (added by , 13 years ago) |
---|
-
django/db/models/manager.py
diff --git a/django/db/models/manager.py b/django/db/models/manager.py index 4fa4c4a..1a2372b 100644
a b class Manager(object): 176 176 def values_list(self, *args, **kwargs): 177 177 return self.get_query_set().values_list(*args, **kwargs) 178 178 179 def namedtuples(self, *args, **kwargs): 180 return self.get_query_set().namedtuples(*args, **kwargs) 181 179 182 def update(self, *args, **kwargs): 180 183 return self.get_query_set().update(*args, **kwargs) 181 184 -
django/db/models/query.py
diff --git a/django/db/models/query.py b/django/db/models/query.py index 6a6a829..66bb57b 100644
a b from django.db.models.query_utils import (Q, select_related_descend, 12 12 deferred_class_factory, InvalidQuery) 13 13 from django.db.models.deletion import Collector 14 14 from django.db.models import signals, sql 15 from django.utils.datastructures import namedtuple 15 16 16 17 # Used to control how many objects are worked with at once in some cases (e.g. 17 18 # when deleting objects). … … class QuerySet(object): 513 514 return self._clone(klass=ValuesListQuerySet, setup=True, flat=flat, 514 515 _fields=fields) 515 516 517 def namedtuples(self, *fields, **kwargs): 518 return self._clone(klass=NamedTuplesQuerySet, setup=True, _fields=fields) 519 516 520 def dates(self, field_name, kind, order='ASC'): 517 521 """ 518 522 Returns a list of datetime objects representing all available dates for … … class ValuesListQuerySet(ValuesQuerySet): 999 1003 return clone 1000 1004 1001 1005 1006 class NamedTuplesQuerySet(ValuesQuerySet): 1007 def iterator(self): 1008 # get field names 1009 extra_names = self.query.extra_select.keys() 1010 field_names = self.field_names 1011 aggregate_names = self.query.aggregate_select.keys() 1012 names = extra_names + field_names + aggregate_names 1013 1014 # create named tuple class 1015 tuple_cls = namedtuple('%sTuple' % self.model.__name__, names) 1016 1017 # wrap every string with our named tuple 1018 for row in self.query.get_compiler(self.db).results_iter(): 1019 yield tuple_cls._make(row) 1020 1021 1002 1022 class DateQuerySet(QuerySet): 1003 1023 def iterator(self): 1004 1024 return self.query.get_compiler(self.db).results_iter() -
django/utils/datastructures.py
diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py index 46f705f..a82b79a 100644
a b 1 1 import copy 2 2 from types import GeneratorType 3 try: 4 from collections import namedtuple 5 except ImportError: 6 namedtuple = None 3 7 4 8 class MergeDict(object): 5 9 """ … … class DictWrapper(dict): 503 507 if use_func: 504 508 return self.func(value) 505 509 return value 510 511 if namedtuple is None: 512 ## {{{ http://code.activestate.com/recipes/500261/ (r15) 513 from operator import itemgetter as _itemgetter 514 from keyword import iskeyword as _iskeyword 515 import sys as _sys 516 517 def namedtuple(typename, field_names, verbose=False, rename=False): 518 """Returns a new subclass of tuple with named fields. 519 520 >>> Point = namedtuple('Point', 'x y') 521 >>> Point.__doc__ # docstring for the new class 522 'Point(x, y)' 523 >>> p = Point(11, y=22) # instantiate with positional args or keywords 524 >>> p[0] + p[1] # indexable like a plain tuple 525 33 526 >>> x, y = p # unpack like a regular tuple 527 >>> x, y 528 (11, 22) 529 >>> p.x + p.y # fields also accessable by name 530 33 531 >>> d = p._asdict() # convert to a dictionary 532 >>> d['x'] 533 11 534 >>> Point(**d) # convert from a dictionary 535 Point(x=11, y=22) 536 >>> p._replace(x=100) # _replace() is like str.replace() but targets named fields 537 Point(x=100, y=22) 538 539 """ 540 541 # Parse and validate the field names. Validation serves two purposes, 542 # generating informative error messages and preventing template injection attacks. 543 if isinstance(field_names, basestring): 544 field_names = field_names.replace(',', ' ').split() # names separated by whitespace and/or commas 545 field_names = tuple(map(str, field_names)) 546 if rename: 547 names = list(field_names) 548 seen = set() 549 for i, name in enumerate(names): 550 if (not min(c.isalnum() or c=='_' for c in name) or _iskeyword(name) 551 or not name or name[0].isdigit() or name.startswith('_') 552 or name in seen): 553 names[i] = '_%d' % i 554 seen.add(name) 555 field_names = tuple(names) 556 for name in (typename,) + field_names: 557 if not min(c.isalnum() or c=='_' for c in name): 558 raise ValueError('Type names and field names can only contain alphanumeric characters and underscores: %r' % name) 559 if _iskeyword(name): 560 raise ValueError('Type names and field names cannot be a keyword: %r' % name) 561 if name[0].isdigit(): 562 raise ValueError('Type names and field names cannot start with a number: %r' % name) 563 seen_names = set() 564 for name in field_names: 565 if name.startswith('_') and not rename: 566 raise ValueError('Field names cannot start with an underscore: %r' % name) 567 if name in seen_names: 568 raise ValueError('Encountered duplicate field name: %r' % name) 569 seen_names.add(name) 570 571 # Create and fill-in the class template 572 numfields = len(field_names) 573 argtxt = repr(field_names).replace("'", "")[1:-1] # tuple repr without parens or quotes 574 reprtxt = ', '.join('%s=%%r' % name for name in field_names) 575 template = '''class %(typename)s(tuple): 576 '%(typename)s(%(argtxt)s)' \n 577 __slots__ = () \n 578 _fields = %(field_names)r \n 579 def __new__(_cls, %(argtxt)s): 580 return _tuple.__new__(_cls, (%(argtxt)s)) \n 581 @classmethod 582 def _make(cls, iterable, new=tuple.__new__, len=len): 583 'Make a new %(typename)s object from a sequence or iterable' 584 result = new(cls, iterable) 585 if len(result) != %(numfields)d: 586 raise TypeError('Expected %(numfields)d arguments, got %%d' %% len(result)) 587 return result \n 588 def __repr__(self): 589 return '%(typename)s(%(reprtxt)s)' %% self \n 590 def _asdict(self): 591 'Return a new dict which maps field names to their values' 592 return dict(zip(self._fields, self)) \n 593 def _replace(_self, **kwds): 594 'Return a new %(typename)s object replacing specified fields with new values' 595 result = _self._make(map(kwds.pop, %(field_names)r, _self)) 596 if kwds: 597 raise ValueError('Got unexpected field names: %%r' %% kwds.keys()) 598 return result \n 599 def __getnewargs__(self): 600 return tuple(self) \n\n''' % locals() 601 for i, name in enumerate(field_names): 602 template += ' %s = _property(_itemgetter(%d))\n' % (name, i) 603 if verbose: 604 print template 605 606 # Execute the template string in a temporary namespace 607 namespace = dict(_itemgetter=_itemgetter, __name__='namedtuple_%s' % typename, 608 _property=property, _tuple=tuple) 609 try: 610 exec template in namespace 611 except SyntaxError, e: 612 raise SyntaxError(e.message + ':\n' + template) 613 result = namespace[typename] 614 615 # For pickling to work, the __module__ variable needs to be set to the frame 616 # where the named tuple is created. Bypass this step in enviroments where 617 # sys._getframe is not defined (Jython for example) or sys._getframe is not 618 # defined for arguments greater than 0 (IronPython). 619 try: 620 result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__') 621 except (AttributeError, ValueError): 622 pass 623 624 return result 625 ## end of http://code.activestate.com/recipes/500261/ }}} -
tests/modeltests/lookup/tests.py
diff --git a/tests/modeltests/lookup/tests.py b/tests/modeltests/lookup/tests.py index 3f40bf1..52a86ae 100644
a b from datetime import datetime 2 2 from operator import attrgetter 3 3 from django.core.exceptions import FieldError 4 4 from django.db import connection 5 from django.utils.datastructures import namedtuple 5 6 from django.test import TestCase, skipUnlessDBFeature 6 7 from models import Author, Article, Tag 7 8 … … class LookupTests(TestCase): 318 319 ], transform=identity) 319 320 self.assertRaises(TypeError, Article.objects.values_list, 'id', 'headline', flat=True) 320 321 322 def test_namedtuples(self): 323 """ 324 namedtuples() is similar to values() and values_list(), except that 325 the results are returned as a Named Tuple, that allows queryset to 326 behave both like list and dictionary. 327 """ 328 identity = lambda x: x 329 330 def assert_namedtuple_equals(queryset, fields, values): 331 cls_name = queryset.model.__name__ 332 tuple_cls = namedtuple(cls_name + 'Tuple', fields) 333 queryset = queryset.namedtuples(*fields) 334 values = [tuple_cls(*value) for value in values] 335 self.assertQuerysetEqual(queryset, values, transform=identity) 336 337 assert_namedtuple_equals( 338 Article.objects.all(), 339 ['headline'], 340 [ 341 (u'Article 5',), 342 (u'Article 6',), 343 (u'Article 4',), 344 (u'Article 2',), 345 (u'Article 3',), 346 (u'Article 7',), 347 (u'Article 1',), 348 ] 349 ) 350 351 assert_namedtuple_equals( 352 Author.objects.order_by('name', 'article__headline', 'article__tag__name'), 353 ['name', 'article__headline', 'article__tag__name'], 354 [ 355 (self.au1.name, self.a1.headline, self.t1.name), 356 (self.au1.name, self.a2.headline, self.t1.name), 357 (self.au1.name, self.a3.headline, self.t1.name), 358 (self.au1.name, self.a3.headline, self.t2.name), 359 (self.au1.name, self.a4.headline, self.t2.name), 360 (self.au2.name, self.a5.headline, self.t2.name), 361 (self.au2.name, self.a5.headline, self.t3.name), 362 (self.au2.name, self.a6.headline, self.t3.name), 363 (self.au2.name, self.a7.headline, self.t3.name), 364 ] 365 ) 366 367 article = Article.objects.namedtuples('headline')[0] 368 self.assertEqual(article[0], article.headline) 369 321 370 def test_get_next_previous_by(self): 322 371 # Every DateField and DateTimeField creates get_next_by_FOO() and 323 372 # get_previous_by_FOO() methods. In the case of identical date values,