# 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