-from .tables import *
+from .tables import Table
from .columns import *
:class:`Column` objects control the way a column (including the cells that
fall within it) are rendered.
+ :param verbose_name: A pretty human readable version of the column name.
+ Typically this is used in the header cells in the HTML output.
+
+ :type accessor: :class:`basestring` or :class:`~.utils.Accessor`
+ :param accessor: An accessor that describes how to extract values for this
+ column from the :term:`table data`.
+
+ :param default: The default value for the column. This can be a value or a
+ callable object [1]_. If an object in the data provides :const:`None`
+ for a column, the default will be used instead.
+
+ The default value may affect ordering, depending on the type of
+ data the table is using. The only case where ordering is not
+ affected ing when a :class:`QuerySet` is used as the table data
+ (since sorting is performed by the database).
+
+ .. [1] The provided callable object must not expect to receive any
+ arguments.
+
+ :type visible: :class:`bool`
+ :param visible: If :const:`False`, this column will not be in HTML from
+ output generators (e.g. :meth:`as_html` or ``{% render_table %}``).
+
+ When a field is not visible, it is removed from the table's
+ :attr:`~Column.columns` iterable.
+
+ :type sortable: :class:`bool`
+ :param sortable: If :const:`False`, this column will not be allowed to
+ influence row ordering/sorting.
+
"""
#: Tracks each time a Column instance is created. Used to retain order.
creation_counter = 0
def __init__(self, verbose_name=None, accessor=None, default=None,
visible=True, sortable=None):
- """Initialise a :class:`Column` object.
-
- :param verbose_name:
- A pretty human readable version of the column name. Typically this
- is used in the header cells in the HTML output.
-
- :param accessor:
- A string or callable that specifies the attribute to access when
- retrieving the value for a cell in this column from the data-set.
- Multiple lookups can be achieved by providing a dot separated list
- of lookups, e.g. ``"user.first_name"``. The functionality is
- identical to that of Django's template variable syntax, e.g. ``{{
- user.first_name }}``
-
- A callable should be used if the dot separated syntax is not
- capable of describing the lookup properly. The callable will be
- passed a single item from the data (if the table is using
- :class:`QuerySet` data, this would be a :class:`Model` instance),
- and is expected to return the correct value for the column.
-
- Consider the following:
-
- .. code-block:: python
-
- >>> import django_tables as tables
- >>> data = [
- ... {'dot.separated.key': 1},
- ... {'dot.separated.key': 2},
- ... ]
- ...
- >>> class SlightlyComplexTable(tables.Table):
- >>> dot_seperated_key = tables.Column(accessor=lambda x: x['dot.separated.key'])
- ...
- >>> table = SlightlyComplexTable(data)
- >>> for row in table.rows:
- >>> print row['dot_seperated_key']
- ...
- 1
- 2
-
- This would **not** have worked:
-
- .. code-block:: python
-
- dot_seperated_key = tables.Column(accessor='dot.separated.key')
-
- :param default:
- The default value for the column. This can be a value or a callable
- object [1]_. If an object in the data provides :const:`None` for a
- column, the default will be used instead.
-
- The default value may affect ordering, depending on the type of
- data the table is using. The only case where ordering is not
- affected ing when a :class:`QuerySet` is used as the table data
- (since sorting is performed by the database).
-
- .. [1] The provided callable object must not expect to receive any
- arguments.
-
- :param visible:
- If :const:`False`, this column will not be in HTML from output
- generators (e.g. :meth:`as_html` or ``{% render_table %}``).
-
- When a field is not visible, it is removed from the table's
- :attr:`~Column.columns` iterable.
-
- :param sortable:
- If :const:`False`, this column will not be allowed to be used in
- ordering the table.
-
- """
if not (accessor is None or isinstance(accessor, basestring) or
callable(accessor)):
raise TypeError('accessor must be a string or callable, not %s' %
@property
def default(self):
- """The default value for cells in this column.
+ """
+ The default value for cells in this column.
The default value passed into ``Column.default`` property may be a
callable, this function handles access.
"""
return self._default() if callable(self._default) else self._default
+ @property
+ def header(self):
+ """
+ The value used for the column heading (e.g. inside the ``<th>`` tag).
+
+ By default this equivalent to the column's :attr:`verbose_name`.
+
+ .. note::
+
+ This property typically isn't accessed directly when a table is
+ rendered. Instead, :attr:`.BoundColumn.header` is accessed which
+ in turn accesses this property. This allows the header to fallback
+ to the column name (it's only available on a :class:`.BoundColumn`
+ object hence accessing that first) when this property doesn't
+ return something useful.
+
+ """
+ return self.verbose_name
+
def render(self, value, **kwargs):
- """Returns a cell's content.
- This method can be overridden by ``render_FOO`` methods on the table or
+ """
+ Returns the content for a specific cell.
+
+ This method can be overridden by :meth:`render_FOO` methods on the table or
by subclassing :class:`Column`.
"""
class CheckBoxColumn(Column):
- """A subclass of Column that renders its column data as a checkbox"""
- def __init__(self, attrs=None, **extra):
- """
- :param attrs: a dict of HTML element attributes to be added to the
- ``<input>``
+ """
+ A subclass of :class:`.Column` that renders as a checkbox form input.
- """
+ This column allows a user to *select* a set of rows. The selection
+ information can then be used to apply some operation (e.g. "delete") onto
+ the set of objects that correspond to the selected rows.
+
+ The value that is extracted from the :term:`table data` for this column is
+ used as the value for the checkbox, i.e. ``<input type="checkbox"
+ value="..." />``
+
+ By default this column is not sortable.
+
+ .. note:: The "apply some operation onto the selection" functionality is
+ not implemented in this column, and requires manually implemention.
+
+ :param attrs:
+ a :class:`dict` of HTML attributes that are added to the rendered
+ ``<input type="checkbox" .../>`` tag
+
+ """
+ def __init__(self, attrs=None, **extra):
params = {'sortable': False}
params.update(extra)
super(CheckBoxColumn, self).__init__(**params)
self.attrs = attrs or {}
- self.verbose_name = mark_safe('<input type="checkbox"/>')
+
+ @property
+ def header(self):
+ return mark_safe('<input type="checkbox"/>')
def render(self, value, bound_column, **kwargs):
attrs = AttributeDict({
'value': value
})
attrs.update(self.attrs)
- return mark_safe('<input %s/>' % AttributeDict(attrs).as_html())
+ return mark_safe('<input %s/>' % attrs.as_html())
class LinkColumn(Column):
+ """
+ A subclass of :class:`.Column` that renders the cell value as a hyperlink.
+
+ It's common to have the primary value in a row hyperlinked to page
+ dedicated to that record.
+
+ The first arguments are identical to that of
+ :func:`django.core.urlresolvers.reverse` and allow a URL to be
+ described. The last argument ``attrs`` allows custom HTML attributes to
+ be added to the ``<a>`` tag.
+
+ :param viewname: See :func:`django.core.urlresolvers.reverse`.
+ :param urlconf: See :func:`django.core.urlresolvers.reverse`.
+ :param args: See :func:`django.core.urlresolvers.reverse`. **
+ :param kwargs: See :func:`django.core.urlresolvers.reverse`. **
+ :param current_app: See :func:`django.core.urlresolvers.reverse`.
+
+ :param attrs:
+ a :class:`dict` of HTML attributes that are added to the rendered
+ ``<input type="checkbox" .../>`` tag
+
+ ** In order to create a link to a URL that relies on information in the
+ current row, :class:`.Accessor` objects can be used in the ``args`` or
+ ``kwargs`` arguments. The accessor will be resolved using the row's record
+ before ``reverse()`` is called.
+
+ Example:
+
+ .. code-block:: python
+
+ # models.py
+ class Person(models.Model):
+ name = models.CharField(max_length=200)
+
+ # urls.py
+ urlpatterns = patterns('',
+ url('people/(\d+)/', views.people_detail, name='people_detail')
+ )
+
+ # tables.py
+ from django_tables.utils import A # alias for Accessor
+
+ class PeopleTable(tables.Table):
+ name = tables.LinkColumn('people_detail', args=[A('pk')])
+
+ """
def __init__(self, viewname, urlconf=None, args=None, kwargs=None,
current_app=None, attrs=None, **extra):
- """
- The first arguments are identical to that of
- :func:`django.core.urlresolvers.reverse` and allow a URL to be
- described. The last argument ``attrs`` allows custom HTML attributes to
- be added to the ``<a>`` tag.
- """
super(LinkColumn, self).__init__(**extra)
self.viewname = viewname
self.urlconf = urlconf
class TemplateColumn(Column):
+ """
+ A subclass of :class:`.Column` that renders some template code to use as
+ the cell value.
+
+ :type template_code: :class:`basestring` object
+ :param template_code: the template code to render
+
+ A :class:`django.templates.Template` object is created from the
+ *template_code* and rendered with a context containing only a ``record``
+ variable. This variable is the record for the table row being rendered.
+
+ Example:
+
+ .. code-block:: python
+
+ class SimpleTable(tables.Table):
+ name1 = tables.TemplateColumn('{{ record.name }}')
+ name2 = tables.Column()
+
+ Both columns will have the same output.
+
+ """
def __init__(self, template_code=None, **extra):
super(TemplateColumn, self).__init__(**extra)
self.template_code = template_code
For convenience, all :class:`Column` properties are available from this
class.
- """
- def __init__(self, table, column, name):
- """Initialise a :class:`BoundColumn` object where:
- * *table* - a :class:`Table` object in which this column exists
- * *column* - a :class:`Column` object
- * *name* – the variable name used when the column was added to the
- :class:`Table` subclass
+ :type table: :class:`Table` object
+ :param table: the table in which this column exists
- """
+ :type column: :class:`Column` object
+ :param column: the type of column
+
+ :type name: :class:`basestring` object
+ :param name: the variable name of the column used to when defining the
+ :class:`Table`. Example:
+
+ .. code-block:: python
+
+ class SimpleTable(tables.Table):
+ age = tables.Column()
+
+ `age` is the name.
+
+ """
+ def __init__(self, table, column, name):
self._table = table
self._column = column
self._name = name
def __unicode__(self):
return self.verbose_name
- @property
- def table(self):
- """Returns the :class:`Table` object that this column is part of."""
- return self._table
-
@property
def column(self):
"""Returns the :class:`Column` object for this column."""
return self._column
- @property
- def name(self):
- """Returns the string used to identify this column."""
- return self._name
-
@property
def accessor(self):
- """Returns the string used to access data for this column out of the
- data source.
+ """
+ Returns the string used to access data for this column out of the data
+ source.
"""
return self.column.accessor or A(self.name)
"""Returns the default value for this column."""
return self.column.default
+ @property
+ def header(self):
+ """
+ Return the value that should be used in the header cell for this
+ column.
+
+ """
+ return self.verbose_name
+
+ @property
+ def name(self):
+ """Returns the string used to identify this column."""
+ return self._name
+
+ @property
+ def order_by(self):
+ """
+ If this column is sorted, return the associated :class:`.OrderBy`
+ instance, otherwise :const:`None`.
+
+ """
+ try:
+ return self.table.order_by[self.name]
+ except IndexError:
+ return None
+
@property
def sortable(self):
- """Returns a ``bool`` depending on whether this column is sortable."""
+ """
+ Return a :class:`bool` depending on whether this column is
+ sortable.
+
+ """
if self.column.sortable is not None:
return self.column.sortable
elif self.table._meta.sortable is not None:
else:
return True # the default value
+ @property
+ def table(self):
+ """Return the :class:`Table` object that this column is part of."""
+ return self._table
+
@property
def verbose_name(self):
- """Returns the verbose name for this column."""
+ """
+ Return the verbose name for this column, or fallback to prettified
+ column name.
+
+ """
return (self.column.verbose_name
or capfirst(force_unicode(self.name.replace('_', ' '))))
@property
def visible(self):
- """Returns a ``bool`` depending on whether this column is visible."""
- return self.column.visible
-
- @property
- def order_by(self):
- """If this column is sorted, return the associated OrderBy instance.
- Otherwise return a None.
+ """
+ Returns a :class:`bool` depending on whether this column is visible.
"""
- try:
- return self.table.order_by[self.name]
- except IndexError:
- return None
+ return self.column.visible
-class Columns(object):
- """Container for spawning BoundColumns.
+class BoundColumns(object):
+ """
+ Container for spawning BoundColumns.
- This is bound to a table and provides its ``columns`` property. It
- provides access to those columns in different ways (iterator,
- item-based, filtered and unfiltered etc), stuff that would not be
- possible with a simple iterator in the table class.
+ This is bound to a table and provides its :attr:`.Table.columns` property.
+ It provides access to those columns in different ways (iterator,
+ item-based, filtered and unfiltered etc), stuff that would not be possible
+ with a simple iterator in the table class.
- A :class:`Columns` object is a container for holding :class:`BoundColumn`
- objects. It provides methods that make accessing columns easier than if
- they were stored in a ``list`` or ``dict``. :class:`Columns` has a similar
- API to a ``dict`` (it actually uses a :class:`SortedDict` interally).
+ A :class:`BoundColumns` object is a container for holding
+ :class:`BoundColumn` objects. It provides methods that make accessing
+ columns easier than if they were stored in a :class:`list` or
+ :class:`dict`. :class:`Columns` has a similar API to a :class:`dict` (it
+ actually uses a :class:`SortedDict` interally).
At the moment you'll only come across this class when you access a
- :attr:`Table.columns` property.
+ :attr:`.Table.columns` property.
+
+ :type table: :class:`.Table` object
+ :param table: the table containing the columns
"""
def __init__(self, table):
- """Initialise a :class:`Columns` object.
-
- *table* must be a :class:`Table` object.
-
- """
self.table = table
# ``self._columns`` attribute stores the bound columns (columns that
# have a real name, )
self._columns = SortedDict()
def _spawn_columns(self):
- # (re)build the "_columns" cache of BoundColumn objects (note that
- # ``base_columns`` might have changed since last time); creating
- # BoundColumn instances can be costly, so we reuse existing ones.
- new_columns = SortedDict()
+ """
+ (re)build the "_bound_columns" cache of :class:`.BoundColumn` objects
+ (note that :attr:`.base_columns` might have changed since last time);
+ creating :class:`.BoundColumn` instances can be costly, so we reuse
+ existing ones.
+
+ """
+ columns = SortedDict()
for name, column in self.table.base_columns.items():
if name in self._columns:
- new_columns[name] = self._columns[name]
+ columns[name] = self._columns[name]
else:
- new_columns[name] = BoundColumn(self.table, column, name)
- self._columns = new_columns
+ columns[name] = BoundColumn(self.table, column, name)
+ self._columns = columns
def all(self):
- """Iterate through all :class:`BoundColumn` objects, regardless of
- visiblity or sortability.
+ """
+ Return an iterator that exposes all :class:`.BoundColumn` objects,
+ regardless of visiblity or sortability.
"""
self._spawn_columns()
yield column
def items(self):
- """Return an iterator of ``(name, column)`` pairs (where *column* is a
- :class:`BoundColumn` object).
+ """
+ Return an iterator of ``(name, column)`` pairs (where ``column`` is a
+ :class:`.BoundColumn` object).
"""
self._spawn_columns()
yield r
def sortable(self):
- """Same as :meth:`all` but only returns sortable :class:`BoundColumn`
+ """
+ Same as :meth:`.BoundColumns.all` but only returns sortable :class:`BoundColumn`
objects.
This is useful in templates, where iterating over the full
yield column
def visible(self):
- """Same as :meth:`sortable` but only returns visible
- :class:`BoundColumn` objects.
+ """
+ Same as :meth:`.sortable` but only returns visible
+ :class:`.BoundColumn` objects.
This is geared towards table rendering.
return item in self
-class Rows(object):
- """Container for spawning BoundRows.
+class BoundRows(object):
+ """
+ Container for spawning :class:`.BoundRow` objects.
+
+ The :attr:`.tables.Table.rows` attribute is a :class:`.BoundRows` object.
+ It provides functionality that would not be possible with a simple iterator
+ in the table class.
- This is bound to a table and provides it's ``rows`` property. It
- provides functionality that would not be possible with a simple
- iterator in the table class.
"""
def __init__(self, table):
- """Initialise a :class:`Rows` object. *table* is the :class:`Table`
- object in which the rows exist.
+ """
+ Initialise a :class:`Rows` object. *table* is the :class:`Table` object
+ in which the rows exist.
"""
self.table = table
def all(self):
- """Return an iterable for all :class:`BoundRow` objects in the table.
+ """
+ Return an iterable for all :class:`BoundRow` objects in the table.
"""
- for row in self.table.data:
- yield BoundRow(self.table, row)
+ for record in self.table.data:
+ yield BoundRow(self.table, record)
def page(self):
- """If the table is paginated, return an iterable of :class:`BoundRow`
- objects that appear on the current page, otherwise return None.
+ """
+ If the table is paginated, return an iterable of :class:`.BoundRow`
+ objects that appear on the current page, otherwise :const:`None`.
"""
if not hasattr(self.table, 'page'):
return iter(self.table.page.object_list)
def __iter__(self):
- """Convience method for all()"""
+ """Convience method for :meth:`.BoundRows.all`"""
return self.all()
def __len__(self):
from django.template import Context
from django.utils.encoding import StrAndUnicode
from .utils import OrderBy, OrderByTuple, Accessor, AttributeDict
-from .columns import Column
-from .rows import Rows, BoundRow
-from .columns import Columns
+from .rows import BoundRows, BoundRow
+from .columns import BoundColumns, Column
-__all__ = ('Table',)
QUERYSET_ACCESSOR_SEPARATOR = '__'
class TableData(object):
- """Exposes a consistent API for a table data. It currently supports a
- :class:`QuerySet` or a ``list`` of ``dict``s.
+ """
+ Exposes a consistent API for :term:`table data`. It currently supports a
+ :class:`QuerySet`, or a :class:`list` of :class:`dict` objects.
+
+ This class is used by :class:.Table` to wrap any
+ input table data.
"""
def __init__(self, data, table):
else len(self.list))
def order_by(self, order_by):
- """Order the data based on column names in the table."""
+ """
+ Order the data based on column names in the table.
+
+ :param order_by: the ordering to apply
+ :type order_by: an :class:`~.utils.OrderByTuple` object
+
+ """
# translate order_by to something suitable for this data
order_by = self._translate_order_by(order_by)
if hasattr(self, 'queryset'):
return OrderByTuple(translated)
def _populate_missing_values(self, data):
- """Populates self._data with missing values based on the default value
+ """
+ Populates self._data with missing values based on the default value
for each column. It will create new items in the dataset (not modify
existing ones).
class DeclarativeColumnsMetaclass(type):
- """Metaclass that converts Column attributes on the class to a dictionary
+ """
+ Metaclass that converts Column attributes on the class to a dictionary
called ``base_columns``, taking into account parent class ``base_columns``
as well.
class TableOptions(object):
- """Options for a :term:`table`.
-
- The following parameters are extracted via attribute access from the
- *object* parameter.
-
- :param sortable:
- bool determining if the table supports sorting.
- :param order_by:
- tuple describing the fields used to order the contents.
- :param attrs:
- HTML attributes added to the ``<table>`` tag.
-
+ """
+ Extracts and exposes options for a :class:`.Table` from a ``class Meta``
+ when the table is defined.
"""
def __init__(self, options=None):
+ """
+
+ :param options: options for a table
+ :type options: :class:`Meta` on a :class:`.Table`
+
+ """
super(TableOptions, self).__init__()
self.sortable = getattr(options, 'sortable', None)
order_by = getattr(options, 'order_by', ())
class Table(StrAndUnicode):
- """A collection of columns, plus their associated data rows."""
+ """A collection of columns, plus their associated data rows.
+
+ :type data: :class:`list` or :class:`QuerySet`
+ :param data:
+ The :term:`table data`.
+
+ :param: :class:`tuple`-like or :class:`basestring`
+ :param order_by:
+ The description of how the table should be ordered. This allows the
+ :attr:`.Table.Meta.order_by` option to be overridden.
+
+ .. note::
+ Unlike a :class:`Form`, tables are always bound to data.
+
+ """
__metaclass__ = DeclarativeColumnsMetaclass
# this value is not the same as None. it means 'use the default sort
TableDataClass = TableData
def __init__(self, data, order_by=DefaultOrder):
- """Create a new table instance with the iterable ``data``.
-
- :param order_by:
- If specified, it must be a sequence containing the names of columns
- in the order that they should be ordered (much the same as
- :method:`QuerySet.order_by`)
-
- If not specified, the table will fall back to the
- :attr:`Meta.order_by` setting.
-
- Note that unlike a ``Form``, tables are always bound to data. Also
- unlike a form, the ``columns`` attribute is read-only and returns
- ``BoundColumn`` wrappers, similar to the ``BoundField``s you get
- when iterating over a form. This is because the table iterator
- already yields rows, and we need an attribute via which to expose
- the (visible) set of (bound) columns - ``Table.columns`` is simply
- the perfect fit for this. Instead, ``base_colums`` is copied to
- table instances, so modifying that will not touch the class-wide
- column list.
-
- """
- self._rows = Rows(self) # bound rows
- self._columns = Columns(self) # bound columns
+ self._rows = BoundRows(self) # bound rows
+ self._columns = BoundColumns(self) # bound columns
self._data = self.TableDataClass(data=data, table=self)
# None is a valid order, so we must use DefaultOrder as a flag
"""The attributes that should be applied to the ``<table>`` tag when
rendering HTML.
- ``attrs`` is an :class:`AttributeDict` object which allows the
- attributes to be rendered to HTML element style syntax via the
- :meth:`~AttributeDict.as_html` method.
+ :returns: :class:`~.utils.AttributeDict` object.
"""
return self._meta.attrs
<thead>
<tr class="{% cycle "odd" "even" %}">
{% for column in table.columns %}
- <th>{{ column }}</th>
+ <th>{{ column.header }}</th>
{% endfor %}
</tr>
</thead>
-{% load django_tables %}
{% spaceless %}
+{% load django_tables %}
<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
<thead>
<tr class="{% cycle "odd" "even" %}">
{% for column in table.columns %}
{% if column.sortable %}
{% with column.order_by as ob %}
- <th class="{% spaceless %}{% if column.sortable %}sortable {% endif %}{% if ob %}{% if ob.is_descending %}desc{% else %}asc{% endif %}{% endif %}{% endspaceless %}"><a href="{% if ob %}{% set_url_param sort=ob.opposite %}{% else %}{% set_url_param sort=column.name %}{% endif %}">{{ column.verbose_name }}</a></th>
+ <th class="{% spaceless %}{% if column.sortable %}sortable {% endif %}{% if ob %}{% if ob.is_descending %}desc{% else %}asc{% endif %}{% endif %}{% endspaceless %}"><a href="{% if ob %}{% set_url_param sort=ob.opposite %}{% else %}{% set_url_param sort=column.name %}{% endif %}">{{ column.header }}</a></th>
{% endwith %}
{% else %}
<th>{{ column.verbose_name }}</th>
class OrderBy(str):
- """A single element in an :class:`OrderByTuple`. This class is essentially
- just a :class:`str` with some extra properties.
+ """A single item in an :class:`.OrderByTuple` object. This class is
+ essentially just a :class:`str` with some extra properties.
"""
@property
def bare(self):
- """Return the bare or naked version. That is, remove a ``-`` prefix if
- it exists and return the result.
+ """
+ Return the :term:`bare <bare orderby>` form.
+
+ :rtype: :class:`.OrderBy` object
"""
return OrderBy(self[1:]) if self[:1] == '-' else self
@property
def opposite(self):
- """Return the an :class:`OrderBy` object with the opposite sort
- influence. e.g.
+ """
+ Return an :class:`.OrderBy` object with an opposite sort influence.
+
+ Example:
.. code-block:: python
>>> order_by.opposite
'-name'
+ :rtype: :class:`.OrderBy` object
+
"""
return OrderBy(self[1:]) if self.is_descending else OrderBy('-' + self)
@property
def is_descending(self):
- """Return :const:`True` if this object induces *descending* ordering."""
+ """
+ Return :const:`True` if this object induces *descending* ordering
+
+ :rtype: :class:`bool`
+
+ """
return self.startswith('-')
@property
def is_ascending(self):
- """Return :const:`True` if this object induces *ascending* ordering."""
+ """
+ Return :const:`True` if this object induces *ascending* ordering.
+
+ :returns: :class:`bool`
+
+ """
return not self.is_descending
class OrderByTuple(tuple, StrAndUnicode):
- """Stores ordering instructions (as :class:`OrderBy` objects). The
- :attr:`Table.order_by` property is always converted into an
- :class:`OrderByTuplw` objectUsed to render output in a format we understand
- as input (see :meth:`~OrderByTuple.__unicode__`) - especially useful in
- templates.
+ """Stores ordering as (as :class:`.OrderBy` objects). The
+ :attr:`django_tables.tables.Table.order_by` property is always converted
+ to an :class:`.OrderByTuple` object.
+
+ This class is essentially just a :class:`tuple` with some useful extras.
- It's quite easy to create one of these. Pass in an iterable, and it will
- automatically convert each element into an :class:`OrderBy` object. e.g.
+ Example:
.. code-block:: python
- >>> ordering = ('name', '-age')
- >>> order_by_tuple = OrderByTuple(ordering)
- >>> age = order_by_tuple['age']
- >>> age
+ >>> x = OrderByTuple(('name', '-age'))
+ >>> x['age']
'-age'
- >>> age.is_descending
+ >>> x['age'].is_descending
True
- >>> age.opposite
+ >>> x['age'].opposite
'age'
"""
return ','.join(self)
def __contains__(self, name):
- """Determine whether a column is part of this order (i.e. descending
- prefix agnostic). e.g.
+ """
+ Determine if a column has an influence on ordering.
+
+ Example:
.. code-block:: python
- >>> ordering = ('name', '-age')
- >>> order_by_tuple = OrderByTuple(ordering)
- >>> 'age' in order_by_tuple
+ >>> ordering =
+ >>> x = OrderByTuple(('name', '-age'))
+ >>> 'age' in x
True
- >>> '-age' in order_by_tuple
+ >>> '-age' in x
True
+ :param name: The name of a column. (optionally prefixed)
+ :returns: :class:`bool`
+
"""
for o in self:
if o == name or o.bare == name:
return False
def __getitem__(self, index):
- """Allows an :class:`OrderBy` object to be extracted using
- :class:`dict`-style indexing in addition to standard 0-based integer
- indexing. The :class:`dict`-style is prefix agnostic in the same way as
- :meth:`~OrderByTuple.__contains__`.
+ """
+ Allows an :class:`.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
- >>> ordering = ('name', '-age')
- >>> order_by_tuple = OrderByTuple(ordering)
- >>> order_by_tuple['age']
+ >>> x = OrderByTuple(('name', '-age'))
+ >>> x[0]
+ 'name'
+ >>> x['age']
'-age'
- >>> order_by_tuple['-age']
+ >>> x['-age']
'-age'
+ :rtype: :class:`.OrderBy` object
+
"""
if isinstance(index, basestring):
for ob in self:
@property
def cmp(self):
- """Return a function suitable for sorting a list. This is used for
- non-:class:`QuerySet` data sources.
+ """
+ Return a function for use with :meth:`list.sort()` that implements this
+ object's ordering. This is used to sort non-:class:`QuerySet` based
+ :term:`table data`.
+
+ :rtype: function
"""
def _cmp(a, b):
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):
- # Try to resolve relationships spanning attributes. This is
- # basically a copy/paste from django/template/base.py in
- # Variable._resolve_lookup()
+ """
+ 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: :class:`object`
+ :param context: The root/first object to traverse.
+ :returns: target object
+ :raises: TypeError, AttributeError, KeyError, ValueError
+
+ :meth:`~.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.
+
+ """
current = context
for bit in self.bits:
try: # dictionary lookup
"""A wrapper around :class:`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 :meth:`.as_html` for a usage example.
+
"""
def as_html(self):
- """Render as HTML style tag attributes."""
+ """
+ Render to HTML tag attributes.
+
+ Example:
+
+ .. code-block:: python
+
+ >>> from django_tables.utils import AttributeDict
+ >>> attrs = AttributeDict({'class': 'mytable', 'id': 'someid'})
+ >>> attrs.as_html()
+ 'class="mytable" id="someid"'
+
+ :rtype: :class:`~django.utils.safestring.SafeUnicode` object
+
+ """
return mark_safe(' '.join(['%s="%s"' % (k, escape(v))
for k, v in self.iteritems()]))
# built documents.
#
# The short X.Y version.
-version = '0.4.0.beta'
+version = '0.4.0'
# The full version, including alpha/beta/rc tags.
-release = '0.4.0.beta'
+release = '0.4.0.beta2'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
.. default-domain:: py
-=====================================================
+===============================================
django-tables - An app for creating HTML tables
-=====================================================
+===============================================
django-tables simplifies the task of turning sets of datainto HTML tables. It
has native support for pagination and sorting. It does for HTML tables what
Quick start guide
=================
-1. Download and install the package.
-2. Install the tables framework by adding ``'django_tables'`` to your
+1. Download and install from https://github.com/bradleyayers/django-tables.
+ Grab a ``.tar.gz`` of the latest tag, and run ``pip install <tar.gz>``.
+2. Hook the app into your Django project by adding ``'django_tables'`` to your
``INSTALLED_APPS`` setting.
-3. Ensure that ``'django.core.context_processors.request'`` is in your
- ``TEMPLATE_CONTEXT_PROCESSORS`` setting.
-4. Write table classes for the types of tables you want to display.
-5. Create an instance of a table in a view, provide it your data, and pass it
- to a template for display.
-6. Use ``{{ table.as_html }}``, the
- :ref:`template tag <template_tags.render_table>`, or your own
- custom template code to display the table.
+3. Write a subclass of :class:`~django_tables.tables.Table` that describes the
+ structure of your table.
+4. Create an instance of your table in a :term:`view`, provide it with
+ :term:`table data`, and pass it to a :term:`template` for display.
+5. Use ``{{ table.as_html }}``, the
+ :ref:`template tag <template-tags.render_table>`, or your own
+ :ref:`custom template <custom-template>` to display the table.
-Tables
-======
+Slow start guide
+================
-For each type of table you want to display, you will need to create a subclass
-of ``django_tables.Table`` that describes the structure of the table.
-
-In this example we are going to take some data describing three countries and
-turn it into a HTML table. We start by creating our data:
+We're going to take some data that describes three countries and
+turn it into an HTML table. This is the data we'll be using:
.. code-block:: python
- >>> countries = [
- ... {'name': 'Australia', 'population': 21, 'tz': 'UTC +10', 'visits': 1},
- ... {'name': 'Germany', 'population', 81, 'tz': 'UTC +1', 'visits': 2},
- ... {'name': 'Mexico', 'population': 107, 'tz': 'UTC -6', 'visits': 0},
- ... ]
+ countries = [
+ {'name': 'Australia', 'population': 21, 'tz': 'UTC +10', 'visits': 1},
+ {'name': 'Germany', 'population', 81, 'tz': 'UTC +1', 'visits': 2},
+ {'name': 'Mexico', 'population': 107, 'tz': 'UTC -6', 'visits': 0},
+ ]
+
-Next we subclass ``django_tables.Table`` to create a table that describes our
-data. The API should look very familiar since it's based on Django's
-database model API:
+The first step is to subclass :class:`~django_tables.tables.Table` and describe
+the table structure. This is done by creating a column for each attribute in
+the :term:`table data`.
.. code-block:: python
- >>> import django_tables as tables
- >>> class CountryTable(tables.Table):
- ... name = tables.Column()
- ... population = tables.Column()
- ... tz = tables.Column(verbose_name='Time Zone')
- ... visits = tables.Column()
+ import django_tables as tables
+ class CountryTable(tables.Table):
+ name = tables.Column()
+ population = tables.Column()
+ tz = tables.Column(verbose_name='Time Zone')
+ visits = tables.Column()
-Providing data
---------------
-To use the table, simply create an instance of the table class and pass in your
-data. e.g. following on from the above example:
+Now that we've defined our table, it's ready for use. We simply create an
+instance of it, and pass in our table data.
.. code-block:: python
- >>> table = CountryTable(countries)
+ table = CountryTable(countries)
-Tables have support for any iterable data that contains objects with
-attributes that can be accessed as property or dictionary syntax:
+Now we add it to our template context and render it to HTML. Typically you'd
+write a view that would look something like:
.. code-block:: python
- >>> table = SomeTable([{'a': 1, 'b': 2}, {'a': 4, 'b': 8}]) # valid
- >>> table = SomeTable(SomeModel.objects.all()) # also valid
-
-Each item in the data corresponds to one row in the table. By default, the
-table uses column names as the keys (or attributes) for extracting cell values
-from the data. This can be changed by using the :attr:`~Column.accessor`
-argument.
+ def home(request):
+ table = CountryTable(countries)
+ return render_to_response('home.html', {'table': table},
+ context_instances=RequestContext(request))
-
-Displaying a table
-------------------
-
-There are two ways to display a table, the easiest way is to use the table's
-own ``as_html`` method:
+In your template, the easiest way to :term:`render` the table is via the
+:meth:`~django_tables.tables.Table.as_html` method:
.. code-block:: django
{{ table.as_html }}
-Which will render something like:
+…which will render something like:
-+--------------+------------+---------+
-| Country Name | Population | Tz |
-+==============+============+=========+
-| Australia | 21 | UTC +10 |
-+--------------+------------+---------+
-| Germany | 81 | UTC +1 |
-+--------------+------------+---------+
-| Mexico | 107 | UTC -6 |
-+--------------+------------+---------+
++--------------+------------+---------+--------+
+| Country Name | Population | Tz | Visit |
++==============+============+=========+========+
+| Australia | 21 | UTC +10 | 1 |
++--------------+------------+---------+--------+
+| Germany | 81 | UTC +1 | 2 |
++--------------+------------+---------+--------+
+| Mexico | 107 | UTC -6 | 0 |
++--------------+------------+---------+--------+
-The downside of this approach is that pagination and sorting will not be
-available. These features require the use of the ``{% render_table %}``
-template tag:
+This approach is easy, but it's not fully featured. For slightly more effort,
+you can render a table with sortable columns. For this, you must use the
+template tag.
.. code-block:: django
{% load django_tables %}
{% render_table table %}
-See :ref:`template_tags` for more information.
+See :ref:`template-tags.render_table` for more information.
+The table will be rendered, but chances are it will still look quite ugly. An
+easy way to make it pretty is to use the built-in *paleblue* theme. For this to
+work, you must add a CSS class to the ``<table>`` tag. This can be achieved by
+adding a ``class Meta:`` to the table class and defining a ``attrs`` variable.
-Ordering
---------
+.. code-block:: python
-Controlling the order that the rows are displayed (sorting) is simple, just use
-the :attr:`~Table.order_by` property or pass it in when initialising the
-instance:
+ import django_tables as tables
-.. code-block:: python
+ class CountryTable(tables.Table):
+ name = tables.Column()
+ population = tables.Column()
+ tz = tables.Column(verbose_name='Time Zone')
+ visits = tables.Column()
- >>> # order_by argument when creating table instances
- >>> table = CountryTable(countries, order_by='name, -population')
- >>> table = CountryTable(countries, order_by=('name', '-population'))
- >>> # order_by property on table instances
- >>> table = CountryTable(countries)
- >>> table.order_by = 'name, -population'
- >>> table.order_by = ('name', '-population')
+ class Meta:
+ attrs = {'class': 'paleblue'}
+The last thing to do is to include the stylesheet in the template.
-Customising the output
-======================
+.. code-block:: html
-There are a number of options available for changing the way the table is
-rendered. Each approach provides balance of ease-of-use and control (the more
-control you want, the less easy it is to use).
+ <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}django_tables/themes/paleblue/css/screen.css" />
-CSS
----
+Save your template and reload the page in your browser.
+
+
+.. _table-data:
+
+Table data
+==========
-If you want to affect the appearance of the table using CSS, you probably want
-to add a ``class`` or ``id`` attribute to the ``<table>`` element. This can be
-achieved by specifying an ``attrs`` variable in the table's ``Meta`` class.
+The data used to populate a table is called :term:`table data`. To provide a
+table with data, pass it in as the first argument when instantiating a table.
.. code-block:: python
- >>> import django_tables as tables
- >>> class SimpleTable(tables.Table):
- ... id = tables.Column()
- ... age = tables.Column()
- ...
- ... class Meta:
- ... attrs = {'class': 'mytable'}
- ...
- >>> table = SimpleTable()
- >>> table.as_html()
- '<table class="mytable">...'
+ table = CountryTable(countries) # valid
+ table = CountryTable(Country.objects.all()) # also valid
+
+Each item in the :term:`table data` is called a :term:`record` and is used to
+populate a single row in the table. By default, the table uses column names
+as :term:`accessors <accessor>` to retrieve individual cell values. This can
+be changed via the :attr:`~django_tables.columns.Column.accessor` argument.
+
+Any iterable can be used as table data, and there's builtin support for
+:class:`QuerySet` objects (to ensure they're handled effeciently).
-The :attr:`Table.attrs` property actually returns an :class:`AttributeDict`
-object. These objects are identical to :class:`dict`, but have an
-:meth:`AttributeDict.as_html` method that returns a HTML tag attribute string.
+
+.. _ordering:
+
+Ordering
+========
+
+Changing the table ordering is easy. When creating a
+:class:`~django_tables.tables.Table` object include an `order_by` parameter
+with a tuple that describes the way the ordering should be applied.
.. code-block:: python
- >>> from django_tables.utils import AttributeDict
- >>> attrs = AttributeDict({'class': 'mytable', 'id': 'someid'})
- >>> attrs.as_html()
- 'class="mytable" id="someid"'
+ table = CountryTable(countries, order_by=('name', '-population'))
+ table = CountryTable(countries, order_by='name,-population') # equivalant
+
+Alternatively, the :attr:`~django_tables.tables.Table.order_by` attribute can
+by modified.
+
+ table = CountryTable(countries)
+ table.order_by = ('name', '-population')
+ table.order_by = 'name,-population' # equivalant
-The returned string is marked safe, so it can be used safely in a template.
-Column formatter
-----------------
+.. _custom-rendering:
-Using a formatter is a quick way to adjust the way values are displayed in a
-column. A limitation of this approach is that you *only* have access to a
-single attribute of the data source.
+Custom rendering
+================
-To use a formatter, simply provide the :attr:`~Column.formatter` argument to a
-:class:`Column` when you define the :class:`Table`:
+Various options are available for changing the way the table is :term:`rendered
+<render>`. Each approach has a different balance of ease-of-use and
+flexibility.
+
+CSS
+---
+
+In order to use CSS to style a table, you'll probably want to add a
+``class`` or ``id`` attribute to the ``<table>`` element. ``django-tables`` has
+a hook that allows abitrary attributes to be added to the ``<table>`` tag.
.. code-block:: python
>>> import django_tables as tables
>>> class SimpleTable(tables.Table):
- ... id = tables.Column(formatter=lambda x: '#%d' % x)
- ... age = tables.Column(formatter=lambda x: '%d years old' % x)
+ ... id = tables.Column()
+ ... age = tables.Column()
...
- >>> table = SimpleTable([{'age': 31, 'id': 10}, {'age': 34, 'id': 11}])
- >>> row = table.rows[0]
- >>> for cell in row:
- ... print cell
+ ... class Meta:
+ ... attrs = {'class': 'mytable'}
...
- #10
- 31 years old
-
-As you can see, the only the value of the column is available to the formatter.
-This means that **it's impossible create a formatter that incorporates other
-values of the record**, e.g. a column with an ``<a href="...">`` that uses
-:func:`reverse` with the record's ``pk``.
+ >>> table = SimpleTable()
+ >>> table.as_html()
+ '<table class="mytable">...'
-If formatters aren't powerful enough, you'll need to either :ref:`create a
-Column subclass <subclassing-column>`, or to use the
-:ref:`Table.render_FOO method <table.render_foo>`.
+Inspired by Django's ORM, the ``class Meta:`` allows you to define extra
+characteristics of a table. See :class:`Table.Meta` for details.
.. _table.render_foo:
-:meth:`Table.render_FOO` Method
--------------------------------
+:meth:`Table.render_FOO` Methods
+--------------------------------
+
+If you want to adjust the way table cells in a particular column are rendered,
+you can implement a ``render_FOO`` method. ``FOO`` is replaced with the
+:term:`name <column name>` of the column.
This approach provides a lot of control, but is only suitable if you intend to
customise the rendering for a single table (otherwise you'll end up having to
copy & paste the method to every table you want to modify – which violates
DRY).
-The example below has a number of different techniques in use:
-
-* :meth:`Column.render` (accessible via :attr:`BoundColumn.column`) applies the
- *formatter* if it's been provided. The effect of this behaviour can be seen
- below in the output for the ``id`` column. Square brackets (from the
- *formatter*) have been applied *after* the angled brackets (from the
- :meth:`~Table.render_FOO`).
-* Completely abitrary values can be returned by :meth:`render_FOO` methods, as
- shown in :meth:`~SimpleTable.render_row_number` (a :attr:`_counter` attribute
- is added to the :class:`SimpleTable` object to keep track of the row number).
+For convenience, a bunch of commonly used/useful values are passed to
+``render_FOO`` functions, when writing the signature, accept the arguments
+you're interested in, and collect the rest in a ``**kwargs`` argument.
- This is possible because :meth:`render_FOO` methods override the default
- behaviour of retrieving a value from the data-source.
+:param value: the value for the cell retrieved from the :term:`table data`
+:param record: the entire record for the row from :term:`table data`
+:param column: the :class:`.Column` object
+:param bound_column: the :class:`.BoundColumn` object
+:param bound_row: the :class:`.BoundRow` object
+:param table: alias for ``self``
.. code-block:: python
>>> import django_tables as tables
>>> class SimpleTable(tables.Table):
... row_number = tables.Column()
- ... id = tables.Column(formatter=lambda x: '[%s]' % x)
- ... age = tables.Column(formatter=lambda x: '%d years old' % x)
+ ... id = tables.Column()
+ ... age = tables.Column()
...
- ... def render_row_number(self, bound_column, bound_row):
+ ... def render_row_number(self, **kwargs):
... value = getattr(self, '_counter', 0)
... self._counter = value + 1
... return 'Row %d' % value
...
- ... def render_id(self, bound_column, bound_row):
- ... value = bound_column.column.render(table=self,
- ... bound_column=bound_column,
- ... bound_row=bound_row)
+ ... def render_id(self, value, **kwargs):
... return '<%s>' % value
...
>>> table = SimpleTable([{'age': 31, 'id': 10}, {'age': 34, 'id': 11}])
... print cell
...
Row 0
- <[10]>
- 31 years old
+ <10>
+ 31
-The :meth:`Column.render` method is what actually performs the lookup into a
-record to retrieve the column value. In the example above, the
-:meth:`render_row_number` never called :meth:`Column.render` and as a result
-there was not attempt to access the data source to retrieve a value.
+.. _custom-template:
Custom Template
---------------
Template tags
=============
-.. _template_tags.render_table:
+.. _template-tags.render_table:
render_table
------------
-If you want to render a table that provides support for sorting and pagination,
-you must use the ``{% render_table %}`` template tag. In this example ``table``
-is an instance of a :class:`django_tables.Table` that has been put into the
-template context:
+Renders a :class:`~django_tables.tables.Table` object to HTML and includes as
+many features as possible.
+
+Sample usage:
.. code-block:: django
{% render_table table %}
-.. _template_tags.set_url_param:
+.. _template-tags.set_url_param:
set_url_param
-------------
with your table (e.g. change the ordering), because you will need to create
urls with the appropriate queries.
-Let's assume we have the query-string
-``?search=pirates&sort=name&page=5`` and we want to update the ``sort``
-parameter:
+Let's assume we have the querystring ``?search=pirates&sort=name&page=5`` and
+we want to update the ``sort`` parameter:
.. code-block:: django
API Reference
=============
+:class:`Accessor` Objects:
+--------------------------
+
+.. autoclass:: django_tables.utils.Accessor
+ :members:
+
+
:class:`Table` Objects:
-------------------------
+-----------------------
.. autoclass:: django_tables.tables.Table
- :members:
+
+
+:class:`Table.Meta` Objects:
+----------------------------
+
+.. class:: Table.Meta
+
+ .. attribute:: attrs
+
+ Allows custom HTML attributes to be specified which will be added to
+ the ``<table>`` tag of any table rendered via
+ :meth:`~django_tables.tables.Table.as_html` or the
+ :ref:`template-tags.render_table` template tag.
+
+ Default: ``{}``
+
+ :type: :class:`dict`
+
+ .. attribute:: sortable
+
+ Does the table support ordering?
+
+ Default: :const:`True`
+
+ :type: :class:`bool`
+
+ .. attribute:: order_by
+
+ The default ordering. e.g. ``('name', '-age')``
+
+ Default: ``()``
+
+ :type: :class:`tuple`
+
+
+:class:`TableData` Objects:
+------------------------------
+
+.. autoclass:: django_tables.tables.TableData
+ :members: __init__, order_by, __getitem__, __len__
:class:`TableOptions` Objects:
------------------------
.. autoclass:: django_tables.columns.Column
- :members: __init__, default, render
-:class:`Columns` Objects
-------------------------
+:class:`CheckBoxColumn` Objects:
+--------------------------------
-.. autoclass:: django_tables.columns.Columns
- :members: __init__, all, items, names, sortable, visible, __iter__,
+.. autoclass:: django_tables.columns.CheckBoxColumn
+ :members:
+
+
+:class:`LinkColumn` Objects:
+----------------------------
+
+.. autoclass:: django_tables.columns.LinkColumn
+ :members:
+
+
+:class:`TemplateColumn` Objects:
+--------------------------------
+
+.. autoclass:: django_tables.columns.TemplateColumn
+ :members:
+
+
+:class:`BoundColumns` Objects
+-----------------------------
+
+.. autoclass:: django_tables.columns.BoundColumns
+ :members: all, items, names, sortable, visible, __iter__,
__contains__, __len__, __getitem__
----------------------------
.. autoclass:: django_tables.columns.BoundColumn
- :members: __init__, table, column, name, accessor, default, formatter,
- sortable, verbose_name, visible
+ :members:
-:class:`Rows` Objects
----------------------
+:class:`BoundRows` Objects
+--------------------------
-.. autoclass:: django_tables.rows.Rows
- :members: __init__, all, page, __iter__, __len__, count, __getitem__
+.. autoclass:: django_tables.rows.BoundRows
+ :members: __init__, all, page, __iter__, __len__, count
:class:`BoundRow` Objects
-----------------------------
.. autoclass:: django_tables.utils.OrderByTuple
- :members: __contains__, __getitem__, __unicode__
+ :members: __unicode__, __contains__, __getitem__, cmp
Glossary
.. glossary::
+ accessor
+ Refers to an :class:`~django_tables.utils.Accessor` object
+
+ bare orderby
+ The non-prefixed form of an :class:`~django_tables.utils.OrderBy`
+ object. Typically the bare form is just the ascending form.
+
+ Example: ``age`` is the bare form of ``-age``
+
+ column name
+ The name given to a column. In the follow example, the *column name* is
+ ``age``.
+
+ .. code-block:: python
+
+ class SimpleTable(tables.Table):
+ age = tables.Column()
+
table
The traditional concept of a table. i.e. a grid of rows and columns
containing data.
+
+ view
+ A Django view.
+
+ record
+ A single Python object used as the data for a single row.
+
+ render
+ The act of serialising a :class:`~django_tables.tables.Table` into
+ HTML.
+
+ template
+ A Django template.
+
+ table data
+ An interable of :term:`records <record>` that
+ :class:`~django_tables.tables.Table` uses to populate its rows.
setup(
name='django-tables',
- version='0.4.0.beta',
+ version='0.4.0.beta2',
description='Table framework for Django',
author='Bradley Ayers',
# -*- coding: utf8 -*-
-from django_tables.utils import OrderByTuple, OrderBy
+from django_tables.utils import OrderByTuple, OrderBy, Accessor
from attest import Tests, Assert
Assert('b') == b.opposite
Assert(True) == b.is_descending
Assert(False) == b.is_ascending
+
+
+@utils.test
+def accessor():
+ x = Accessor('0')
+ Assert('B') == x.resolve('Brad')
+
+ x = Accessor('1')
+ Assert('r') == x.resolve('Brad')
+
+ x = Accessor('2.upper')
+ Assert('A') == x.resolve('Brad')
+
+ x = Accessor('2.upper.__len__')
+ Assert(1) == x.resolve('Brad')