Repository: encode/django-rest-framework

Issue:
Usage of both @property and __getattr__ can lead to obscure and hard to debug errors
Encountered this error when accessing request.DATA in a view:

``` python
  File "views.py", line 146, in put
    if request.DATA is not None:
  File "/usr/local/lib/python2.7/dist-packages/rest_framework/request.py", line 453, in __getattr__
    return getattr(self._request, attr)
AttributeError: 'WSGIRequest' object has no attribute 'DATA'
```

This was mind-boggling. I couldn't understand why `__getattr__` was ever called on the request object, since a property called DATA already exists (confirmed this via dir(request) before accessing DATA), and as we all know, getattr is only used for undefined properties. 

After an intense debugging session it turned out that this is caused by a "feature" in python. This may be common knowledge, but it definitely was new news to me: if a property getter raises an AttributeError, python (`__getattribute__`) falls back to using `__getattr__`. The issue is that the traceback is lost, which makes it extremely hard to debug, since the actual exception raised isn't shown anywhere. So, the **actual** error was:

```
AttributeError: 'TemporaryFileUploadHandler' object has no attribute 'file'
```

Further explanation/discussion: https://groups.google.com/forum/#!topic/comp.lang.python/BZf-d0rLP8U

I'm not sure if this is an issue that you can/want to address, but I wanted to make people aware of it since it was a pain to track down and others might stumble into the same situation.



Discussion:
Assuming related to #1939. Not clear if something similar to #1938 would fix.

Yes, definitely the same error. Pretty sure the same solution applied in #1938 would fix it. Probably needs fixing in all getters/setters in classes with custom properties and getattr defined.

Another option might be to override `__getattribute__` instead of `__getattr__` and handle it there instead as that'd be more DRY.

Any update on this issue?

Not yet. Plenty of other things going on, but anyone's welcome to dig into this and come up with clear proposal/example pull request etc.

Dang, I'm having the same problem but I can't trace it.

One route someone to start on this would be to demonstrate the most minimal possible python example that demonstates the issue (ie forget about REST framework at all to start with)

That'd give us a much better point to work towards a fix from, as it'd narrow things down to the core issue.

``` python
class Test(object):
    @property
    def foo(self):
        # This will raise an AttributeError, after which
        # __getattr__ will be used as a fallback.
        return self.doesntexist

    def __getattr__(self, name):
        raise AttributeError('No attribute ' + name)

t = Test()
t.foo
```

The output will be

```
Traceback (most recent call last):
  File "2108.py", line 12, in <module>
    t.foo
  File "2108.py", line 9, in __getattr__
    raise AttributeError('No attribute ' + name)
AttributeError: No attribute foo
```

As you can see, the original exception and traceback is lost (from trying to access self.doesntexist).


Resolving patch:
diff --git a/rest_framework/request.py b/rest_framework/request.py
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -12,12 +12,13 @@
 from django.conf import settings
 from django.http import QueryDict
 from django.http.multipartparser import parse_header
+from django.utils import six
 from django.utils.datastructures import MultiValueDict
 from django.utils.datastructures import MergeDict as DjangoMergeDict
-from django.utils.six import BytesIO
 from rest_framework import HTTP_HEADER_ENCODING
 from rest_framework import exceptions
 from rest_framework.settings import api_settings
+import sys
 import warnings
 
 
@@ -362,7 +363,7 @@ def _load_stream(self):
         elif hasattr(self._request, 'read'):
             self._stream = self._request
         else:
-            self._stream = BytesIO(self.raw_post_data)
+            self._stream = six.BytesIO(self.raw_post_data)
 
     def _perform_form_overloading(self):
         """
@@ -404,7 +405,7 @@ def _perform_form_overloading(self):
             self._CONTENTTYPE_PARAM in self._data
         ):
             self._content_type = self._data[self._CONTENTTYPE_PARAM]
-            self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding']))
+            self._stream = six.BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding']))
             self._data, self._files, self._full_data = (Empty, Empty, Empty)
 
     def _parse(self):
@@ -485,8 +486,16 @@ def _not_authenticated(self):
         else:
             self.auth = None
 
-    def __getattr__(self, attr):
+    def __getattribute__(self, attr):
         """
-        Proxy other attributes to the underlying HttpRequest object.
+        If an attribute does not exist on this instance, then we also attempt
+        to proxy it to the underlying HttpRequest object.
         """
-        return getattr(self._request, attr)
+        try:
+            return super(Request, self).__getattribute__(attr)
+        except AttributeError:
+            info = sys.exc_info()
+            try:
+                return getattr(self._request, attr)
+            except AttributeError:
+                six.reraise(info[0], info[1], info[2].tb_next)
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -154,7 +154,9 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
 
     If autoescape is True, the link text and URLs will get autoescaped.
     """
-    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
+    def trim_url(x, limit=trim_url_limit):
+        return limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
+
     safe_input = isinstance(text, SafeData)
     words = word_split_re.split(force_text(text))
     for i, word in enumerate(words):


Test patch:
diff --git a/tests/test_authentication.py b/tests/test_authentication.py
--- a/tests/test_authentication.py
+++ b/tests/test_authentication.py
@@ -205,7 +205,10 @@ def test_post_json_passing_token_auth(self):
     def test_post_json_makes_one_db_query(self):
         """Ensure that authenticating a user using a token performs only one DB query"""
         auth = "Token " + self.key
-        func_to_test = lambda: self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
+
+        def func_to_test():
+            return self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
+
         self.assertNumQueries(1, func_to_test)
 
     def test_post_form_failing_token_auth(self):
diff --git a/tests/test_relations_hyperlink.py b/tests/test_relations_hyperlink.py
--- a/tests/test_relations_hyperlink.py
+++ b/tests/test_relations_hyperlink.py
@@ -12,7 +12,9 @@
 request = factory.get('/')  # Just to ensure we have a request in the serializer context
 
 
-dummy_view = lambda request, pk: None
+def dummy_view(request, pk):
+    pass
+
 
 urlpatterns = patterns(
     '',
diff --git a/tests/test_renderers.py b/tests/test_renderers.py
--- a/tests/test_renderers.py
+++ b/tests/test_renderers.py
@@ -28,8 +28,13 @@
 DUMMYSTATUS = status.HTTP_200_OK
 DUMMYCONTENT = 'dummycontent'
 
-RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii')
-RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii')
+
+def RENDERER_A_SERIALIZER(x):
+    return ('Renderer A: %s' % x).encode('ascii')
+
+
+def RENDERER_B_SERIALIZER(x):
+    return ('Renderer B: %s' % x).encode('ascii')
 
 
 expected_results = [
diff --git a/tests/test_request.py b/tests/test_request.py
--- a/tests/test_request.py
+++ b/tests/test_request.py
@@ -249,9 +249,29 @@ def test_logged_in_user_is_set_on_wrapped_request(self):
         login(self.request, self.user)
         self.assertEqual(self.wrapped_request.user, self.user)
 
+    def test_calling_user_fails_when_attribute_error_is_raised(self):
+        """
+        This proves that when an AttributeError is raised inside of the request.user
+        property, that we can handle this and report the true, underlying error.
+        """
+        class AuthRaisesAttributeError(object):
+            def authenticate(self, request):
+                import rest_framework
+                rest_framework.MISSPELLED_NAME_THAT_DOESNT_EXIST
 
-class TestAuthSetter(TestCase):
+        self.request = Request(factory.get('/'), authenticators=(AuthRaisesAttributeError(),))
+        SessionMiddleware().process_request(self.request)
 
+        login(self.request, self.user)
+        try:
+            self.request.user
+        except AttributeError as error:
+            self.assertEqual(str(error), "'module' object has no attribute 'MISSPELLED_NAME_THAT_DOESNT_EXIST'")
+        else:
+            assert False, 'AttributeError not raised'
+
+
+class TestAuthSetter(TestCase):
     def test_auth_can_be_set(self):
         request = Request(factory.get('/'))
         request.auth = 'DUMMY'
diff --git a/tests/test_response.py b/tests/test_response.py
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -38,8 +38,13 @@ class MockTextMediaRenderer(BaseRenderer):
 DUMMYSTATUS = status.HTTP_200_OK
 DUMMYCONTENT = 'dummycontent'
 
-RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii')
-RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii')
+
+def RENDERER_A_SERIALIZER(x):
+    return ('Renderer A: %s' % x).encode('ascii')
+
+
+def RENDERER_B_SERIALIZER(x):
+    return ('Renderer B: %s' % x).encode('ascii')
 
 
 class RendererA(BaseRenderer):
diff --git a/tests/test_throttling.py b/tests/test_throttling.py
--- a/tests/test_throttling.py
+++ b/tests/test_throttling.py
@@ -188,7 +188,9 @@ def setUp(self):
         class XYScopedRateThrottle(ScopedRateThrottle):
             TIMER_SECONDS = 0
             THROTTLE_RATES = {'x': '3/min', 'y': '1/min'}
-            timer = lambda self: self.TIMER_SECONDS
+
+            def timer(self):
+                return self.TIMER_SECONDS
 
         class XView(APIView):
             throttle_classes = (XYScopedRateThrottle,)
@@ -290,7 +292,9 @@ def setUp(self):
         class Throttle(ScopedRateThrottle):
             THROTTLE_RATES = {'test_limit': '1/day'}
             TIMER_SECONDS = 0
-            timer = lambda self: self.TIMER_SECONDS
+
+            def timer(self):
+                return self.TIMER_SECONDS
 
         class View(APIView):
             throttle_classes = (Throttle,)