When resolving an accessor, *all* exceptions are smothered into None, resolves #98

This commit is contained in:
Bradley Ayers 2012-09-21 12:54:01 +10:00
parent bb4d2a11d0
commit 95f706bf13
6 changed files with 89 additions and 58 deletions

View File

@ -101,28 +101,25 @@ class BoundRow(object):
bound_column = self.table.columns[name]
value = None
try:
# We need to take special care here to allow get_FOO_display()
# methods on a model to be used if available. See issue #30.
path, _, remainder = bound_column.accessor.rpartition('.')
penultimate = A(path).resolve(self.record)
# If the penultimate is a model and the remainder is a field
# using choices, use get_FOO_display().
if isinstance(penultimate, models.Model):
try:
field = penultimate._meta.get_field(remainder)
display = getattr(penultimate, 'get_%s_display' % remainder, None)
if field.choices and display:
value = display()
remainder = None
except FieldDoesNotExist:
pass
# Fall back to just using the original accessor (we just need
# to follow the remainder).
if remainder:
value = A(remainder).resolve(penultimate)
except (TypeError, AttributeError, KeyError, ValueError):
pass
# We need to take special care here to allow get_FOO_display()
# methods on a model to be used if available. See issue #30.
path, _, remainder = bound_column.accessor.rpartition('.')
penultimate = A(path).resolve(self.record, quiet=True)
# If the penultimate is a model and the remainder is a field
# using choices, use get_FOO_display().
if isinstance(penultimate, models.Model):
try:
field = penultimate._meta.get_field(remainder)
display = getattr(penultimate, 'get_%s_display' % remainder, None)
if field.choices and display:
value = display()
remainder = None
except FieldDoesNotExist:
pass
# Fall back to just using the original accessor (we just need
# to follow the remainder).
if remainder:
value = A(remainder).resolve(penultimate, quiet=True)
if value in bound_column.column.empty_values:
return bound_column.default

View File

@ -254,7 +254,7 @@ class Accessor(str):
"""
SEPARATOR = '.'
def resolve(self, context, safe=True):
def resolve(self, context, safe=True, quiet=False):
"""
Return an object described by the accessor by traversing the attributes
of *context*.
@ -270,10 +270,15 @@ class Accessor(str):
>>> x.resolve('brad')
'B'
:type context: :class:`object`
:type context: :class:`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: TypeError, AttributeError, KeyError, ValueError
:raises: anything `getattr(a, "b")` raises, e.g. `TypeError`,
`AttributeError`, `KeyError`, `ValueError`
:meth:`~.Accessor.resolve` attempts lookups in the following order:
@ -284,35 +289,39 @@ class Accessor(str):
Callable objects are called, and their result is used, before
proceeding with the resolving.
"""
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
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):

View File

@ -47,6 +47,7 @@ class Occupation(models.Model):
class Region(models.Model):
name = models.CharField(max_length=200)
mayor = models.OneToOneField(Person, null=True)
def __unicode__(self):
return self.name

View File

@ -1,5 +1,5 @@
# coding: utf-8
from attest import assert_hook, Tests
from attest import assert_hook, Tests # pylint: disable=W0611
import itertools
from django_attest import TestContext
import django_tables2 as tables
@ -278,3 +278,18 @@ def queryset_table_data_supports_ordering():
assert table.rows[0]["first_name"] == "Bradley"
table.order_by = "-first_name"
assert table.rows[0]["first_name"] == "Stevie"
@models.test
def doesnotexist_from_accessor_should_use_default():
class Table(tables.Table):
class Meta:
model = Person
default = "abc"
fields = ("first_name", "last_name", "region")
Person.objects.create(first_name="Brad", last_name="Ayers")
table = Table(Person.objects.all())
assert table.rows[0]["first_name"] == "Brad"
assert table.rows[0]["region"] == "abc"

View File

@ -174,9 +174,11 @@ def render_table_supports_queryset():
template = Template('{% load django_tables2 %}{% render_table qs %}')
html = template.render(Context({'qs': Region.objects.all()}))
root = parse(html)
assert [e.text for e in root.findall('.//thead/tr/th/a')] == ["ID", "Name"]
td = [[td.text for td in tr.findall('td')] for tr in root.findall('.//tbody/tr')]
db = [map(str, v) for v in Region.objects.values_list("id", "name")]
assert [e.text for e in root.findall('.//thead/tr/th/a')] == ["ID", "Name", "Mayor"]
td = [[unicode(td.text) for td in tr.findall('td')] for tr in root.findall('.//tbody/tr')]
db = []
for region in Region.objects.all():
db.append([unicode(region.id), region.name, u""])
assert td == db

View File

@ -73,6 +73,7 @@ def accessor():
def accessor_wont_honors_alters_data():
class Foo(object):
deleted = False
def delete(self):
self.deleted = True
delete.alters_data = True
@ -83,6 +84,12 @@ def accessor_wont_honors_alters_data():
assert foo.deleted == False
@utils.test
def accessor_can_be_quiet():
foo = {}
assert Accessor("bar").resolve(foo, quiet=True) is None
@utils.test
def attribute_dict_handles_escaping():
x = AttributeDict({"x": '"\'x&'})