Repository: Pylons/pyramid

Issue:
@view_defaults not taken into consideration upon resolving conflicts
I've got the following:

```
@view_defaults(containment='TestRoot')
class testNotFound(object):
    def __init__(self, request):
        pass

    @notfound_view_config(renderer='string')
    def notfound(self):
        return "Not Found"

@notfound_view_config(renderer='string', append_slash=True)
def notfound(self):
    return "Not Found"
```

However when the Configurator is done adding all of them, and it attempts to resolve the conflicts it throws an error:

```
Traceback (most recent call last):
  File "view_defaults.py", line 40, in <module>
    app = config.make_wsgi_app()
  File "/Users/xistence/Projects/pyramid/pyramid/config/__init__.py", line 935, in make_wsgi_app
    self.commit()
  File "/Users/xistence/Projects/pyramid/pyramid/config/__init__.py", line 606, in commit
    self.action_state.execute_actions(introspector=self.introspector)
  File "/Users/xistence/Projects/pyramid/pyramid/config/__init__.py", line 1044, in execute_actions
    for action in resolveConflicts(self.actions):
  File "/Users/xistence/Projects/pyramid/pyramid/config/__init__.py", line 1172, in resolveConflicts
    raise ConfigurationConflictError(conflicts)
pyramid.exceptions.ConfigurationConflictError: Conflicting configuration actions
  For: ('view', <class 'pyramid.httpexceptions.HTTPNotFound'>, '', None, 'd41d8cd98f00b204e9800998ecf8427e')
    Line 26 of file view_defaults.py:
        @notfound_view_config(renderer='string', append_slash=True)
    Line 22 of file view_defaults.py:
        @notfound_view_config(renderer='string')
```

I would expect that it would use the defined `view_defaults` to help differentiate between the two different ones. If I add `containment='TestRoot'` to the `@notfound_view_config` everything does work without issues.

Full test case:

```
import os
import logging

from pyramid.config import Configurator
from pyramid.view import notfound_view_config, view_defaults

from wsgiref.simple_server import make_server


logging.basicConfig()
log = logging.getLogger(__file__)
here = os.path.dirname(os.path.abspath(__file__))

class TestRoot(object):
    pass

@view_defaults(containment='TestRoot')
class testNotFound(object):
    def __init__(self, request):
        pass

    @notfound_view_config(renderer='string')
    def notfound(self):
        return "Not Found"

@notfound_view_config(renderer='string', append_slash=True)
def notfound(self):
    return "Not Found"

if __name__ == '__main__':
    # configuration settings
    settings = {}
    config = Configurator(settings=settings)

    # scan for @view_config 
    config.scan()

    # serve app
    app = config.make_wsgi_app()
    server = make_server('0.0.0.0', 8080, app)
    server.serve_forever()
```



Discussion:

Resolving patch:
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -1550,6 +1550,7 @@ def _derive_view(self, view, permission=None, predicates=(),
 
         return deriver(view)
 
+    @viewdefaults
     @action_method
     def add_forbidden_view(
         self,
@@ -1629,6 +1630,7 @@ def forbidden(request):
 
     set_forbidden_view = add_forbidden_view # deprecated sorta-bw-compat alias
     
+    @viewdefaults
     @action_method
     def add_notfound_view(
         self,


Test patch:
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -1815,6 +1815,36 @@ def test_add_forbidden_view_disallows_http_cache(self):
         self.assertRaises(ConfigurationError,
                           config.add_forbidden_view, http_cache='foo')
 
+    def test_add_forbidden_view_with_view_defaults(self):
+        from pyramid.interfaces import IRequest
+        from pyramid.renderers import null_renderer
+        from pyramid.exceptions import PredicateMismatch
+        from pyramid.httpexceptions import HTTPForbidden
+        from zope.interface import directlyProvides
+        from zope.interface import implementedBy
+        class view(object):
+            __view_defaults__ = {
+                'containment':'pyramid.tests.test_config.IDummy'
+                }
+            def __init__(self, request):
+                pass
+            def __call__(self):
+                return 'OK'
+        config = self._makeOne(autocommit=True)
+        config.add_forbidden_view(
+            view=view,
+            renderer=null_renderer)
+        wrapper = self._getViewCallable(
+            config, ctx_iface=implementedBy(HTTPForbidden),
+            request_iface=IRequest)
+        context = DummyContext()
+        directlyProvides(context, IDummy)
+        request = self._makeRequest(config)
+        self.assertEqual(wrapper(context, request), 'OK')
+        context = DummyContext()
+        request = self._makeRequest(config)
+        self.assertRaises(PredicateMismatch, wrapper, context, request)
+
     def test_add_notfound_view(self):
         from pyramid.renderers import null_renderer
         from zope.interface import implementedBy
@@ -1882,6 +1912,36 @@ def view(request): return Response('OK')
         result = view(None, request)
         self.assertEqual(result.location, '/scriptname/foo/?a=1&b=2')
 
+    def test_add_notfound_view_with_view_defaults(self):
+        from pyramid.interfaces import IRequest
+        from pyramid.renderers import null_renderer
+        from pyramid.exceptions import PredicateMismatch
+        from pyramid.httpexceptions import HTTPNotFound
+        from zope.interface import directlyProvides
+        from zope.interface import implementedBy
+        class view(object):
+            __view_defaults__ = {
+                'containment':'pyramid.tests.test_config.IDummy'
+                }
+            def __init__(self, request):
+                pass
+            def __call__(self):
+                return 'OK'
+        config = self._makeOne(autocommit=True)
+        config.add_notfound_view(
+            view=view,
+            renderer=null_renderer)
+        wrapper = self._getViewCallable(
+            config, ctx_iface=implementedBy(HTTPNotFound),
+            request_iface=IRequest)
+        context = DummyContext()
+        directlyProvides(context, IDummy)
+        request = self._makeRequest(config)
+        self.assertEqual(wrapper(context, request), 'OK')
+        context = DummyContext()
+        request = self._makeRequest(config)
+        self.assertRaises(PredicateMismatch, wrapper, context, request)
+
     # Since Python 3 has to be all cool and fancy and different...
     def _assertBody(self, response, value):
         from pyramid.compat import text_type