Ticket #15648: namedtuples.patch

File namedtuples.patch, 11.2 KB (added by Paul Miller <paulmillr@…>, 13 years ago)

Named tuples patch

  • 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):  
    176176    def values_list(self, *args, **kwargs):
    177177        return self.get_query_set().values_list(*args, **kwargs)
    178178
     179    def namedtuples(self, *args, **kwargs):
     180        return self.get_query_set().namedtuples(*args, **kwargs)
     181
    179182    def update(self, *args, **kwargs):
    180183        return self.get_query_set().update(*args, **kwargs)
    181184
  • 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,  
    1212    deferred_class_factory, InvalidQuery)
    1313from django.db.models.deletion import Collector
    1414from django.db.models import signals, sql
     15from django.utils.datastructures import namedtuple
    1516
    1617# Used to control how many objects are worked with at once in some cases (e.g.
    1718# when deleting objects).
    class QuerySet(object):  
    513514        return self._clone(klass=ValuesListQuerySet, setup=True, flat=flat,
    514515                _fields=fields)
    515516
     517    def namedtuples(self, *fields, **kwargs):
     518        return self._clone(klass=NamedTuplesQuerySet, setup=True, _fields=fields)
     519
    516520    def dates(self, field_name, kind, order='ASC'):
    517521        """
    518522        Returns a list of datetime objects representing all available dates for
    class ValuesListQuerySet(ValuesQuerySet):  
    9991003        return clone
    10001004
    10011005
     1006class 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
    10021022class DateQuerySet(QuerySet):
    10031023    def iterator(self):
    10041024        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  
    11import copy
    22from types import GeneratorType
     3try:
     4    from collections import namedtuple
     5except ImportError:
     6    namedtuple = None
    37
    48class MergeDict(object):
    59    """
    class DictWrapper(dict):  
    503507        if use_func:
    504508            return self.func(value)
    505509        return value
     510
     511if 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  
    22from operator import attrgetter
    33from django.core.exceptions import FieldError
    44from django.db import connection
     5from django.utils.datastructures import namedtuple
    56from django.test import TestCase, skipUnlessDBFeature
    67from models import Author, Article, Tag
    78
    class LookupTests(TestCase):  
    318319            ], transform=identity)
    319320        self.assertRaises(TypeError, Article.objects.values_list, 'id', 'headline', flat=True)
    320321
     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
    321370    def test_get_next_previous_by(self):
    322371        # Every DateField and DateTimeField creates get_next_by_FOO() and
    323372        # get_previous_by_FOO() methods. In the case of identical date values,
Back to Top