Repository: Pylons/pyramid

Issue:
Cannot subclass Request and use add_request_method
See this test:

``` python

class TestPyramidRequestProperties:

    def test_it(self):
        from pyramid.request import Request
        from zope.interface import providedBy, implementedBy

        req = Request({})
        assert providedBy(req) == implementedBy(Request)

        req._set_properties({'b':'b'})

        assert providedBy(req) == implementedBy(Request)

    def test_subclassing(self):
        from pyramid.request import Request
        from zope.interface import providedBy, implementedBy

        class Subclass(Request):
            pass

        req = Subclass({})

        #calling providedBy(req) makes the test pass
        #providedBy(req)

        #assert providedBy(req) == implementedBy(Subclass)

        req._set_properties({'b':'b'})

        assert providedBy(req) == implementedBy(Subclass)

```

This is the output from the test:

```
Traceback (most recent call last):
  File "test_request.py", line 111, in test_subclassing
    assert providedBy(req) == implementedBy(Subclass)
AssertionError: assert <implementedB...uiltin__.type> == <implementedBy...uest.Subclass>
  Full diff:
  - <implementedBy __builtin__.type>
  + <implementedBy kotti.tests.test_request.Subclass>
```

And this is the difference from a normal request to one that has been "decorated" by _set_properties:

```

(Pdb) pp vars(providedBy(req))
{'__bases__': (<implementedBy __builtin__.object>,),
 '__iro__': (<InterfaceClass zope.interface.Interface>,),
 '__name__': '__builtin__.type',
 '__sro__': (<implementedBy __builtin__.type>,
             <implementedBy __builtin__.object>,
             <InterfaceClass zope.interface.Interface>),
 '_implied': {<InterfaceClass zope.interface.Interface>: (),
              <implementedBy __builtin__.object>: (),
              <implementedBy __builtin__.type>: ()},
 'dependents': <WeakKeyDictionary at 140434285597760>,
 'inherit': <type 'type'>}

(Pdb) pp vars(providedBy(Subclass({})))
{'__bases__': (<implementedBy pyramid.request.Request>,),
 '__iro__': (<InterfaceClass pyramid.interfaces.IRequest>,
             <InterfaceClass zope.interface.Interface>),
 '__name__': 'kotti.tests.test_request.Subclass',
 '__sro__': (<implementedBy kotti.tests.test_request.Subclass>,
             <implementedBy pyramid.request.Request>,
             <InterfaceClass pyramid.interfaces.IRequest>,
             <InterfaceClass zope.interface.Interface>,
             <implementedBy webob.request.BaseRequest>,
             <implementedBy pyramid.url.URLMethodsMixin>,
             <implementedBy pyramid.request.CallbackMethodsMixin>,
             <implementedBy pyramid.util.InstancePropertyMixin>,
             <implementedBy pyramid.i18n.LocalizerRequestMixin>,
             <implementedBy pyramid.security.AuthenticationAPIMixin>,
             <implementedBy pyramid.security.AuthorizationAPIMixin>,
             <implementedBy __builtin__.object>),
 '_implied': {<InterfaceClass pyramid.interfaces.IRequest>: (),
              <InterfaceClass zope.interface.Interface>: (),
              <implementedBy kotti.tests.test_request.Subclass>: (),
              <implementedBy pyramid.request.Request>: (),
              <implementedBy webob.request.BaseRequest>: (),
              <implementedBy pyramid.url.URLMethodsMixin>: (),
              <implementedBy pyramid.request.CallbackMethodsMixin>: (),
              <implementedBy pyramid.util.InstancePropertyMixin>: (),
              <implementedBy pyramid.i18n.LocalizerRequestMixin>: (),
              <implementedBy pyramid.security.AuthenticationAPIMixin>: (),
              <implementedBy pyramid.security.AuthorizationAPIMixin>: (),
              <implementedBy __builtin__.object>: ()},
 'dependents': <WeakKeyDictionary at 140434219857464>,
 'inherit': <class 'kotti.tests.test_request.Subclass'>}
```

In the first test, `test_it`, the `req` variable has 'proper' providedBy information, only by subclassing the problem appears.



Discussion:
There's a line in the second test, `providedBy(req)`. If it's called before the `_set_properties` is called, then the test passes. Also, after the line `self.__class__ = cls` from pyramid.util is called, `self` will be missing the `__provides__` attribute.

I originally marked this as a bug because I thought you weren't able to subclass request. It appears that actually works fine. I'm not sure what the concern is here. It's entirely valid for an object to have a different list of interfaces than its class object. `Request._set_properties` is actually changing the object's hierarchy to have a phony class in the middle containing new descriptors.

Can you clarify what your concern is?

@mmerickel It is possible to subclass Request, but the provided interfaces by that subclass are no longer the same after calling set_properties. This violates expected zope.interface behaviour.

If `IRequest.providedBy(Request())` is True and equal to `IRequest.providedBy(SubclassofRequest())` then why should this be false after triggering _set_properties on the request?

That phony class in the middle that you talk about should implement the same interfaces as the original request. Why is that important? Because if I call `pyramid.view.render_view_to_response()` with a broken request, then I won't get the proper response, because the view will not be found, the interfaces provided by the request are used as discriminators for the view.

Okay I see what you're saying. I did not realize that the new class did not have the `IRequest` etc interfaces attached. That's definitely an issue.


Resolving patch:
diff --git a/pyramid/util.py b/pyramid/util.py
--- a/pyramid/util.py
+++ b/pyramid/util.py
@@ -87,10 +87,10 @@ def _set_properties(self, properties):
         if attrs:
             parent = self.__class__
             cls = type(parent.__name__, (parent, object), attrs)
-            # We assign __provides__, __implemented__ and __providedBy__ below
-            # to prevent a memory leak that results from from the usage of this
-            # instance's eventual use in an adapter lookup.  Adapter lookup
-            # results in ``zope.interface.implementedBy`` being called with the
+            # We assign __provides__ and __implemented__ below to prevent a
+            # memory leak that results from from the usage of this instance's
+            # eventual use in an adapter lookup.  Adapter lookup results in
+            # ``zope.interface.implementedBy`` being called with the
             # newly-created class as an argument.  Because the newly-created
             # class has no interface specification data of its own, lookup
             # causes new ClassProvides and Implements instances related to our
@@ -99,7 +99,7 @@ def _set_properties(self, properties):
             # want this new class to behave exactly like it is the parent class
             # instead.  See https://github.com/Pylons/pyramid/issues/1212 for
             # more information.
-            for name in ('__implemented__', '__providedBy__', '__provides__'):
+            for name in ('__implemented__', '__provides__'):
                 # we assign these attributes conditionally to make it possible
                 # to test this class in isolation without having any interfaces
                 # attached to it


Test patch:
diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py
--- a/pyramid/tests/test_request.py
+++ b/pyramid/tests/test_request.py
@@ -435,6 +435,64 @@ def test_subpath_path_info_and_script_name_have_utf8(self):
         self.assertEqual(request.environ['SCRIPT_NAME'], '/' + encoded)
         self.assertEqual(request.environ['PATH_INFO'], '/' + encoded)
 
+class Test_subclassing_Request(unittest.TestCase):
+    def test_subclass(self):
+        from pyramid.interfaces import IRequest
+        from pyramid.request import Request
+        from zope.interface import providedBy, implementedBy
+
+        class RequestSub(Request):
+            pass
+
+        self.assertTrue(hasattr(Request, '__provides__'))
+        self.assertTrue(hasattr(Request, '__implemented__'))
+        self.assertTrue(hasattr(Request, '__providedBy__'))
+        self.assertFalse(hasattr(RequestSub, '__provides__'))
+        self.assertTrue(hasattr(RequestSub, '__providedBy__'))
+        self.assertTrue(hasattr(RequestSub, '__implemented__'))
+
+        self.assertTrue(IRequest.implementedBy(RequestSub))
+        # The call to implementedBy will add __provides__ to the class
+        self.assertTrue(hasattr(RequestSub, '__provides__'))
+
+
+    def test_subclass_with_implementer(self):
+        from pyramid.interfaces import IRequest
+        from pyramid.request import Request
+        from zope.interface import providedBy, implementedBy, implementer
+
+        @implementer(IRequest)
+        class RequestSub(Request):
+            pass
+
+        self.assertTrue(hasattr(Request, '__provides__'))
+        self.assertTrue(hasattr(Request, '__implemented__'))
+        self.assertTrue(hasattr(Request, '__providedBy__'))
+        self.assertTrue(hasattr(RequestSub, '__provides__'))
+        self.assertTrue(hasattr(RequestSub, '__providedBy__'))
+        self.assertTrue(hasattr(RequestSub, '__implemented__'))
+
+        req = RequestSub({})
+        req._set_properties({'b': 'b'})
+
+        self.assertTrue(IRequest.providedBy(req))
+        self.assertTrue(IRequest.implementedBy(RequestSub))
+
+    def test_subclass_mutate_before_providedBy(self):
+        from pyramid.interfaces import IRequest
+        from pyramid.request import Request
+        from zope.interface import providedBy, implementedBy, implementer
+
+        class RequestSub(Request):
+            pass
+
+        req = RequestSub({})
+        req._set_properties({'b': 'b'})
+
+        self.assertTrue(IRequest.providedBy(req))
+        self.assertTrue(IRequest.implementedBy(RequestSub))
+
+
 class DummyRequest:
     def __init__(self, environ=None):
         if environ is None: