django_tables2.utils — django-tables2 1.0.4 documentation

# Source code for django_tables2.utils

    # coding: utf-8
    from __future__ import absolute_import, unicode_literals
    from django.core.handlers.wsgi import WSGIRequest
    from django.utils.html import escape
    from django.utils.safestring import mark_safe
    from django.test.client import FakePayload
    from itertools import chain
    import inspect
    import six
    import warnings

    def python_2_unicode_compatible(klass):
        """
        A decorator that defines __unicode__ and __str__ methods under Python 2.
        Under Python 3 it does nothing.

        To support Python 2 and 3 with a single code base, define a __str__ method
        returning text and apply this decorator to the class.

        Taken directly from Django.
        """
        if not six.PY3:
            klass.__unicode__ = klass.__str__
            klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
        return klass

    class Sequence(list):
        """
        Represents a column sequence, e.g. ``("first_name", "...", "last_name")``

        This is used to represent `.Table.Meta.sequence` or the `.Table`
        constructors's *sequence* keyword argument.

        The sequence must be a list of column names and is used to specify the
        order of the columns on a table. Optionally a "..." item can be inserted,
        which is treated as a *catch-all* for column names that aren't explicitly
        specified.
        """
        def expand(self, columns):
            """
            Expands the ``"..."`` item in the sequence into the appropriate column
            names that should be placed there.

            :raises: `ValueError` if the sequence is invalid for the columns.
            """
            ellipses = self.count("...")
            if ellipses > 1:
                raise ValueError("'...' must be used at most once in a sequence.")
            elif ellipses == 0:
                self.append("...")

            # everything looks good, let's expand the "..." item
            columns = list(columns)  # take a copy and exhaust the generator
            head = []
            tail = []
            target = head  # start by adding things to the head
            for name in self:
                if name == "...":
                    # now we'll start adding elements to the tail
                    target = tail
                    continue
                target.append(name)
                if name in columns:
                    columns.pop(columns.index(name))
            self[:] = chain(head, columns, tail)

    class OrderBy(six.text_type):
        """
        A single item in an `.OrderByTuple` object. This class is
        essentially just a `str` with some extra properties.
        """
        @property
        def bare(self):
            """
            Return the bare form.

            The *bare form* is the non-prefixed form. Typically the bare form is
            just the ascending form.

            Example: ``age`` is the bare form of ``-age``

            :rtype: `.OrderBy` object
            """
            return OrderBy(self[1:]) if self[:1] == '-' else self

        @property
        def opposite(self):
            """
            Return an `.OrderBy` object with an opposite sort influence.

            Example:

            .. code-block:: python

                >>> order_by = OrderBy('name')
                >>> order_by.opposite
                '-name'

            :rtype: `.OrderBy` object
            """
            return OrderBy(self[1:]) if self.is_descending else OrderBy('-' + self)

        @property
        def is_descending(self):
            """
            Return `True` if this object induces *descending* ordering

            :rtype: `bool`
            """
            return self.startswith('-')

        @property
        def is_ascending(self):
            """
            Return `True` if this object induces *ascending* ordering.

            :returns: `bool`
            """
            return not self.is_descending

    @python_2_unicode_compatible
    class OrderByTuple(tuple):
        """Stores ordering as (as `.OrderBy` objects). The
        `~django_tables2.tables.Table.order_by` property is always converted
        to an `.OrderByTuple` object.

        This class is essentially just a `tuple` with some useful extras.

        Example:

        .. code-block:: python

            >>> x = OrderByTuple(('name', '-age'))
            >>> x['age']
            '-age'
            >>> x['age'].is_descending
            True
            >>> x['age'].opposite
            'age'

        """
        def __new__(cls, iterable):
            transformed = []
            for item in iterable:
                if not isinstance(item, OrderBy):
                    item = OrderBy(item)
                transformed.append(item)
            return super(OrderByTuple, cls).__new__(cls, transformed)

        def __str__(self):
            return ','.join(self)

        def __contains__(self, name):
            """
            Determine if a column has an influence on ordering.

            Example:

            .. code-block:: python

                >>> ordering =
                >>> x = OrderByTuple(('name', ))
                >>> 'name' in  x
                True
                >>> '-name' in x
                True

            :param name: The name of a column. (optionally prefixed)
            :returns: `bool`
            """
            name = OrderBy(name).bare
            for order_by in self:
                if order_by.bare == name:
                    return True
            return False

        def __getitem__(self, index):
            """
            Allows an `.OrderBy` object to be extracted via named or integer
            based indexing.

            When using named based indexing, it's fine to used a prefixed named.

            .. code-block:: python

                >>> x = OrderByTuple(('name', '-age'))
                >>> x[0]
                'name'
                >>> x['age']
                '-age'
                >>> x['-age']
                '-age'

            :rtype: `.OrderBy` object
            """
            if isinstance(index, six.string_types):
                for order_by in self:
                    if order_by == index or order_by.bare == index:
                        return order_by
                raise KeyError
            return super(OrderByTuple, self).__getitem__(index)

        @property
        def key(self):
            accessors = []
            reversing = []
            for order_by in self:
                accessors.append(Accessor(order_by.bare))
                reversing.append(order_by.is_descending)

            @total_ordering
            class Comparator(object):
                def __init__(self, obj):
                    self.obj = obj

                def __eq__(self, other):
                    for accessor in accessors:
                        a = accessor.resolve(self.obj, quiet=True)
                        b = accessor.resolve(other.obj, quiet=True)
                        if not a == b:
                            return False
                    return True

                def __lt__(self, other):
                    for accessor, reverse in six.moves.zip(accessors, reversing):
                        a = accessor.resolve(self.obj, quiet=True)
                        b = accessor.resolve(other.obj, quiet=True)
                        if a == b:
                            continue
                        if reverse:
                            a, b = b, a
                        # The rest of this should be refactored out into a util
                        # function 'compare' that handles different types.
                        try:
                            return a < b
                        except TypeError:
                            # If the truth values differ, it's a good way to
                            # determine ordering.
                            if bool(a) is not bool(b):
                                return bool(a) < bool(b)
                            # Handle comparing different types, by falling back to
                            # the string and id of the type. This at least groups
                            # different types together.
                            a_type = type(a)
                            b_type = type(b)
                            return (repr(a_type), id(a_type)) < (repr(b_type), id(b_type))
                    return False
            return Comparator

        @property
        def cmp(self):
            """
            Return a function for use with `list.sort` that implements this
            object's ordering. This is used to sort non-`.QuerySet` based
            :term:`table data`.

            :rtype: function
            """
            warnings.warn('`cmp` is deprecated, use `key` instead.',
                          DeprecationWarning)

            # pylint: disable=C0103
            def _cmp(a, b):
                for accessor, reverse in instructions:
                    x = accessor.resolve(a)
                    y = accessor.resolve(b)
                    try:
                        res = cmp(x, y)
                    except TypeError:
                        res = cmp((repr(type(x)), id(type(x)), x),
                                  (repr(type(y)), id(type(y)), y))
                    if res != 0:
                        return -res if reverse else res
                return 0
            instructions = []
            for order_by in self:
                if order_by.startswith('-'):
                    instructions.append((Accessor(order_by[1:]), True))
                else:
                    instructions.append((Accessor(order_by), False))
            return _cmp

        def get(self, key, fallback):
            """
            Identical to __getitem__, but supports fallback value.
            """
            try:
                return self[key]
            except (KeyError, IndexError):
                return fallback

        @property
        def opposite(self):
            """
            Return version with each `.OrderBy` prefix toggled.

            Example:

            .. code-block:: python

                >>> order_by = OrderByTuple(('name', '-age'))
                >>> order_by.opposite
                ('-name', 'age')
            """
            return type(self)((o.opposite for o in self))

    class Accessor(str):
        """
        A string describing a path from one object to another via attribute/index
        accesses. For convenience, the class has an alias `.A` to allow for more concise code.

        Relations are separated by a ``.`` character.
        """
        SEPARATOR = '.'

        def resolve(self, context, safe=True, quiet=False):
            """
            Return an object described by the accessor by traversing the attributes
            of *context*.

            Example:

            .. code-block:: python

                >>> x = Accessor('__len__')
                >>> x.resolve('brad')
                4
                >>> x = Accessor('0.upper')
                >>> x.resolve('brad')
                'B'

            :type  context: `object`
            :param context: The root/first object to traverse.
            :type     safe: `bool`
            :param    safe: Don't call anything with ``alters_data = True``
            :type    quiet: bool
            :param   quiet: Smother all exceptions and instead return `None`
            :returns: target object
            :raises: anything ``getattr(a, "b")`` raises, e.g. `TypeError`,
                     `AttributeError`, `KeyError`, `ValueError` (unless *quiet* ==
                     `True`)

            `~.Accessor.resolve` attempts lookups in the following order:

            - dictionary (e.g. ``obj[related]``)
            - attribute (e.g. ``obj.related``)
            - list-index lookup (e.g. ``obj[int(related)]``)

            Callable objects are called, and their result is used, before
            proceeding with the resolving.
            """
            try:
                current = context
                for bit in self.bits:
                    try:  # dictionary lookup
                        current = current[bit]
                    except (TypeError, AttributeError, KeyError):
                        try:  # attribute lookup
                            current = getattr(current, bit)
                        except (TypeError, AttributeError):
                            try:  # list-index lookup
                                current = current[int(bit)]
                            except (IndexError,  # list index out of range
                                    ValueError,  # invalid literal for int()
                                    KeyError,    # dict without `int(bit)` key
                                    TypeError,   # unsubscriptable object
                                    ):
                                raise ValueError('Failed lookup for key [%s] in %r'
                                                 ', when resolving the accessor %s'
                                                  % (bit, current, self))
                    if callable(current):
                        if safe and getattr(current, 'alters_data', False):
                            raise ValueError('refusing to call %s() because `.alters_data = True`'
                                             % repr(current))
                        current = current()
                    # important that we break in None case, or a relationship
                    # spanning across a null-key will raise an exception in the
                    # next iteration, instead of defaulting.
                    if current is None:
                        break
                return current
            except:
                if not quiet:
                    raise

        @property
        def bits(self):
            if self == '':
                return ()
            return self.split(self.SEPARATOR)

    A = Accessor  # alias

    class AttributeDict(dict):
        """
        A wrapper around `dict` that knows how to render itself as HTML
        style tag attributes.

        The returned string is marked safe, so it can be used safely in a template.
        See `.as_html` for a usage example.
        """
        def as_html(self):
            """
            Render to HTML tag attributes.

            Example:

            .. code-block:: python

                >>> from django_tables2.utils import AttributeDict
                >>> attrs = AttributeDict({'class': 'mytable', 'id': 'someid'})
                >>> attrs.as_html()
                'class="mytable" id="someid"'

            :rtype: `~django.utils.safestring.SafeUnicode` object

            """
            return mark_safe(' '.join(['%s="%s"' % (k, escape(v if not callable(v) else v()))
                                       for k, v in six.iteritems(self)]))

    class Attrs(dict):
        """
        Backwards compatibility, deprecated.
        """
        def __init__(self, *args, **kwargs):
            super(Attrs, self).__init__(*args, **kwargs)
            warnings.warn("Attrs class is deprecated, use dict instead.",
                          DeprecationWarning)

    def segment(sequence, aliases):
        """
        Translates a flat sequence of items into a set of prefixed aliases.

        This allows the value set by `.QuerySet.order_by` to be translated into
        a list of columns that would have the same result. These are called
        "order by aliases" which are optionally prefixed column names.

        e.g.

            >>> list(segment(("a", "-b", "c"),
            ...              {"x": ("a"),
            ...               "y": ("b", "-c"),
            ...               "z": ("-b", "c")}))
            [("x", "-y"), ("x", "z")]

        """
        if not (sequence or aliases):
            return
        for alias, parts in aliases.items():
            variants = {
                # alias: order by tuple
                alias:  OrderByTuple(parts),
                OrderBy(alias).opposite: OrderByTuple(parts).opposite,
            }
            for valias, vparts in variants.items():
                if list(sequence[:len(vparts)]) == list(vparts):
                    tail_aliases = dict(aliases)
                    del tail_aliases[alias]
                    tail_sequence = sequence[len(vparts):]
                    if tail_sequence:
                        for tail in segment(tail_sequence, tail_aliases):
                            yield tuple(chain([valias], tail))
                        else:
                            continue
                    else:
                        yield tuple([valias])

    class cached_property(object):  # pylint: disable=C0103
        """
        Decorator that creates converts a method with a single
        self argument into a property cached on the instance.

        Taken directly from Django 1.4.
        """
        def __init__(self, func):
            from functools import wraps
            wraps(func)(self)
            self.func = func

        def __get__(self, instance, cls):
            res = instance.__dict__[self.func.__name__] = self.func(instance)
            return res

    funcs = (name for name in ('getfullargspec', 'getargspec')
                           if hasattr(inspect, name))
    getargspec = getattr(inspect, next(funcs))
    del funcs

    def build_request(uri='/'):
        """
        Return a fresh HTTP GET / request.

        This is essentially a heavily cutdown version of Django 1.3's
        `~django.test.client.RequestFactory`.
        """
        path, _, querystring = uri.partition('?')
        return WSGIRequest({
            'CONTENT_TYPE':      'text/html; charset=utf-8',
            'PATH_INFO':         path,
            'QUERY_STRING':      querystring,
            'REMOTE_ADDR':       '127.0.0.1',
            'REQUEST_METHOD':    'GET',
            'SCRIPT_NAME':       '',
            'SERVER_NAME':       'testserver',
            'SERVER_PORT':       '80',
            'SERVER_PROTOCOL':   'HTTP/1.1',
            'wsgi.version':      (1, 0),
            'wsgi.url_scheme':   'http',
            'wsgi.input':        FakePayload(b''),
            'wsgi.errors':       six.StringIO(),
            'wsgi.multiprocess': True,
            'wsgi.multithread':  False,
            'wsgi.run_once':     False,
        })

    def total_ordering(cls):
        """Class decorator that fills in missing ordering methods"""
        convert = {
            '__lt__': [('__gt__', lambda self, other: not (self < other or self == other)),
                       ('__le__', lambda self, other: self < other or self == other),
                       ('__ge__', lambda self, other: not self < other)],
            '__le__': [('__ge__', lambda self, other: not self <= other or self == other),
                       ('__lt__', lambda self, other: self <= other and not self == other),
                       ('__gt__', lambda self, other: not self <= other)],
            '__gt__': [('__lt__', lambda self, other: not (self > other or self == other)),
                       ('__ge__', lambda self, other: self > other or self == other),
                       ('__le__', lambda self, other: not self > other)],
            '__ge__': [('__le__', lambda self, other: (not self >= other) or self == other),
                       ('__gt__', lambda self, other: self >= other and not self == other),
                       ('__lt__', lambda self, other: not self >= other)]
        }
        roots = set(dir(cls)) & set(convert)
        if not roots:
            raise ValueError('must define at least one ordering operation: < > <= >=')
        root = max(roots)       # prefer __lt__ to __le__ to __gt__ to __ge__
        for opname, opfunc in convert[root]:
            if opname not in roots:
                opfunc.__name__ = str(opname)  # Py2 requires non-unicode, Py3 requires unicode.
                opfunc.__doc__ = getattr(int, opname).__doc__
                setattr(cls, opname, opfunc)
        return cls

    def computed_values(d):
        """
        Computes a new `dict` that has callable values replaced with the return values.

        Simple example:

            >>> compute_values({"foo": lambda: "bar"})
            {"foo": "bar"}

        Arbitrarily deep structures are supported. The logic is as follows:

        1. If the value is callable, call it and make that the new value.
        2. If the value is an instance of dict, use ComputableDict to compute its keys.

        Example:

            >>> def parents():
            ...     return {
            ...         "father": lambda: "Foo",
            ...         "mother": "Bar"
            ...      }
            ...
            >>> a = {
            ...     "name": "Brad",
            ...     "parents": parents
            ... }
            ...
            >>> computed_values(a)
            {"name": "Brad", "parents": {"father": "Foo", "mother": "Bar"}}

        :rtype: dict
        """
        result = {}
        for k, v in six.iteritems(d):
            if callable(v):
                v = v()
            if isinstance(v, dict):
                v = computed_values(v)
            result[k] = v
        return result