When resolving an accessor, *all* exceptions are smothered into None, resolves #98
This commit is contained in:
parent
bb4d2a11d0
commit
95f706bf13
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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&'})
|
||||
|
|
Loading…
Reference in New Issue