Skip to content

Commit 873fb69

Browse files
committed
Merge pull request #2530 from tomchristie/attribute-proxying-fix
Fix misleading `AttributeError` tracebacks on `Request` objects.
2 parents 235b98e + 1a087c8 commit 873fb69

File tree

9 files changed

+73
-27
lines changed

9 files changed

+73
-27
lines changed

.gitignore

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,14 @@
33
*~
44
.*
55

6-
site/
7-
htmlcov/
8-
coverage/
9-
build/
10-
dist/
11-
*.egg-info/
6+
/site/
7+
/htmlcov/
8+
/coverage/
9+
/build/
10+
/dist/
11+
/*.egg-info/
12+
/env/
1213
MANIFEST
1314

14-
bin/
15-
include/
16-
lib/
17-
local/
18-
1915
!.gitignore
2016
!.travis.yml

rest_framework/request.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@
1212
from django.conf import settings
1313
from django.http import QueryDict
1414
from django.http.multipartparser import parse_header
15+
from django.utils import six
1516
from django.utils.datastructures import MultiValueDict
1617
from django.utils.datastructures import MergeDict as DjangoMergeDict
17-
from django.utils.six import BytesIO
1818
from rest_framework import HTTP_HEADER_ENCODING
1919
from rest_framework import exceptions
2020
from rest_framework.settings import api_settings
21+
import sys
2122
import warnings
2223

2324

@@ -362,7 +363,7 @@ def _load_stream(self):
362363
elif hasattr(self._request, 'read'):
363364
self._stream = self._request
364365
else:
365-
self._stream = BytesIO(self.raw_post_data)
366+
self._stream = six.BytesIO(self.raw_post_data)
366367

367368
def _perform_form_overloading(self):
368369
"""
@@ -404,7 +405,7 @@ def _perform_form_overloading(self):
404405
self._CONTENTTYPE_PARAM in self._data
405406
):
406407
self._content_type = self._data[self._CONTENTTYPE_PARAM]
407-
self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding']))
408+
self._stream = six.BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding']))
408409
self._data, self._files, self._full_data = (Empty, Empty, Empty)
409410

410411
def _parse(self):
@@ -485,8 +486,16 @@ def _not_authenticated(self):
485486
else:
486487
self.auth = None
487488

488-
def __getattr__(self, attr):
489+
def __getattribute__(self, attr):
489490
"""
490-
Proxy other attributes to the underlying HttpRequest object.
491+
If an attribute does not exist on this instance, then we also attempt
492+
to proxy it to the underlying HttpRequest object.
491493
"""
492-
return getattr(self._request, attr)
494+
try:
495+
return super(Request, self).__getattribute__(attr)
496+
except AttributeError:
497+
info = sys.exc_info()
498+
try:
499+
return getattr(self._request, attr)
500+
except AttributeError:
501+
six.reraise(info[0], info[1], info[2].tb_next)

rest_framework/templatetags/rest_framework.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,9 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
154154
155155
If autoescape is True, the link text and URLs will get autoescaped.
156156
"""
157-
trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
157+
def trim_url(x, limit=trim_url_limit):
158+
return limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
159+
158160
safe_input = isinstance(text, SafeData)
159161
words = word_split_re.split(force_text(text))
160162
for i, word in enumerate(words):

tests/test_authentication.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,10 @@ def test_post_json_passing_token_auth(self):
205205
def test_post_json_makes_one_db_query(self):
206206
"""Ensure that authenticating a user using a token performs only one DB query"""
207207
auth = "Token " + self.key
208-
func_to_test = lambda: self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
208+
209+
def func_to_test():
210+
return self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
211+
209212
self.assertNumQueries(1, func_to_test)
210213

211214
def test_post_form_failing_token_auth(self):

tests/test_relations_hyperlink.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
request = factory.get('/') # Just to ensure we have a request in the serializer context
1313

1414

15-
dummy_view = lambda request, pk: None
15+
def dummy_view(request, pk):
16+
pass
17+
1618

1719
urlpatterns = patterns(
1820
'',

tests/test_renderers.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,13 @@
2828
DUMMYSTATUS = status.HTTP_200_OK
2929
DUMMYCONTENT = 'dummycontent'
3030

31-
RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii')
32-
RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii')
31+
32+
def RENDERER_A_SERIALIZER(x):
33+
return ('Renderer A: %s' % x).encode('ascii')
34+
35+
36+
def RENDERER_B_SERIALIZER(x):
37+
return ('Renderer B: %s' % x).encode('ascii')
3338

3439

3540
expected_results = [

tests/test_request.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,29 @@ def test_logged_in_user_is_set_on_wrapped_request(self):
249249
login(self.request, self.user)
250250
self.assertEqual(self.wrapped_request.user, self.user)
251251

252+
def test_calling_user_fails_when_attribute_error_is_raised(self):
253+
"""
254+
This proves that when an AttributeError is raised inside of the request.user
255+
property, that we can handle this and report the true, underlying error.
256+
"""
257+
class AuthRaisesAttributeError(object):
258+
def authenticate(self, request):
259+
import rest_framework
260+
rest_framework.MISSPELLED_NAME_THAT_DOESNT_EXIST
252261

253-
class TestAuthSetter(TestCase):
262+
self.request = Request(factory.get('/'), authenticators=(AuthRaisesAttributeError(),))
263+
SessionMiddleware().process_request(self.request)
254264

265+
login(self.request, self.user)
266+
try:
267+
self.request.user
268+
except AttributeError as error:
269+
self.assertEqual(str(error), "'module' object has no attribute 'MISSPELLED_NAME_THAT_DOESNT_EXIST'")
270+
else:
271+
assert False, 'AttributeError not raised'
272+
273+
274+
class TestAuthSetter(TestCase):
255275
def test_auth_can_be_set(self):
256276
request = Request(factory.get('/'))
257277
request.auth = 'DUMMY'

tests/test_response.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,13 @@ class MockTextMediaRenderer(BaseRenderer):
3838
DUMMYSTATUS = status.HTTP_200_OK
3939
DUMMYCONTENT = 'dummycontent'
4040

41-
RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii')
42-
RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii')
41+
42+
def RENDERER_A_SERIALIZER(x):
43+
return ('Renderer A: %s' % x).encode('ascii')
44+
45+
46+
def RENDERER_B_SERIALIZER(x):
47+
return ('Renderer B: %s' % x).encode('ascii')
4348

4449

4550
class RendererA(BaseRenderer):

tests/test_throttling.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,9 @@ def setUp(self):
188188
class XYScopedRateThrottle(ScopedRateThrottle):
189189
TIMER_SECONDS = 0
190190
THROTTLE_RATES = {'x': '3/min', 'y': '1/min'}
191-
timer = lambda self: self.TIMER_SECONDS
191+
192+
def timer(self):
193+
return self.TIMER_SECONDS
192194

193195
class XView(APIView):
194196
throttle_classes = (XYScopedRateThrottle,)
@@ -290,7 +292,9 @@ def setUp(self):
290292
class Throttle(ScopedRateThrottle):
291293
THROTTLE_RATES = {'test_limit': '1/day'}
292294
TIMER_SECONDS = 0
293-
timer = lambda self: self.TIMER_SECONDS
295+
296+
def timer(self):
297+
return self.TIMER_SECONDS
294298

295299
class View(APIView):
296300
throttle_classes = (Throttle,)

0 commit comments

Comments
 (0)