debian-django-tables2/django_tables2/utils.py

596 lines
19 KiB
Python

# 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