From 5fc0d36724a6197c8c0106e846d8e78e1219b1fe Mon Sep 17 00:00:00 2001
From: Chris McDonough <chrism@plope.com>
Date: Wed, 31 Jul 2013 02:30:07 +0200
Subject: [PATCH] first cut at indicating that only some predicates are negatable

---
 pyramid/config/util.py                 |    8 +++
 docs/narr/viewconfig.rst               |   18 +++++++--
 pyramid/config/predicates.py           |    8 ++++
 pyramid/config/__init__.py             |   12 +++++
 pyramid/tests/test_config/test_util.py |   26 +++++++++---
 5 files changed, 59 insertions(+), 13 deletions(-)

diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst
index fd32293..9292548 100644
--- a/docs/narr/viewconfig.rst
+++ b/docs/narr/viewconfig.rst
@@ -560,8 +560,8 @@
 Inverting Predicate Values
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-You can invert the meaning of any predicate value by wrapping it in a call to
-:class:`pyramid.config.not_`.
+You can invert the meaning of a supported predicate value by wrapping it in a
+call to :class:`pyramid.config.not_`.
 
 .. code-block:: python
    :linenos:
@@ -571,11 +571,21 @@
    config.add_view(
        'mypackage.views.my_view',
        route_name='ok',
-       request_method=not_('POST')
+       header=not_('X-Foo')
        )
 
 The above example will ensure that the view is called if the request method
-is *not* ``POST``, at least if no other view is more specific.
+does not have the ``X-Foo`` header, at least if no other view is more specific.
+
+A predicate must be *negatable* to use ``not_`` against its value.  The current
+set of built-in negatable predicates are: ``path_info``, ``request_param``,
+``header``, ``containment``, ``request_type``, ``match_param``,
+``physical_path``, and ``effective_principals``.  If you try to use ``not_``
+against a non-negatable predicate, an error will be raised at startup time.
+
+You can make sure a custom view predicate added via
+:meth:`pyramid.config.Configurator.add_view_predicate` is negatable by adding a
+``negatable`` attribute to it that is True.
 
 This technique of wrapping a predicate value in ``not_`` can be used anywhere
 predicate values are accepted:
diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py
index d52ee0e..698c943 100644
--- a/pyramid/config/__init__.py
+++ b/pyramid/config/__init__.py
@@ -1152,7 +1152,17 @@
             # and "i" exist for sorting purposes after conflict resolution.
             ainfo = (order, i, action)
 
-            discriminator = undefer(action['discriminator'])
+            try:
+                discriminator = undefer(action['discriminator'])
+            except:
+                t, v, tb = sys.exc_info()
+                try:
+                    reraise(ConfigurationExecutionError,
+                            ConfigurationExecutionError(t, v, action['info']),
+                            tb)
+                finally:
+                   del t, v, tb
+                
             action['discriminator'] = discriminator
 
             if discriminator is None:
diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py
index c8f66e8..6caa875 100644
--- a/pyramid/config/predicates.py
+++ b/pyramid/config/predicates.py
@@ -48,6 +48,7 @@
         return request.method in self.val
 
 class PathInfoPredicate(object):
+    negatable = True
     def __init__(self, val, config):
         self.orig = val
         try:
@@ -65,6 +66,7 @@
         return self.val.match(request.upath_info) is not None
     
 class RequestParamPredicate(object):
+    negatable = True
     def __init__(self, val, config):
         val = as_sorted_tuple(val)
         reqs = []
@@ -95,6 +97,7 @@
         return True
 
 class HeaderPredicate(object):
+    negatable = True
     def __init__(self, val, config):
         name = val
         v = None
@@ -137,6 +140,7 @@
         return self.val in request.accept
 
 class ContainmentPredicate(object):
+    negatable = True
     def __init__(self, val, config):
         self.val = config.maybe_dotted(val)
 
@@ -150,6 +154,7 @@
         return find_interface(ctx, self.val) is not None
     
 class RequestTypePredicate(object):
+    negatable = True
     def __init__(self, val, config):
         self.val = val
 
@@ -162,6 +167,7 @@
         return self.val.providedBy(request)
     
 class MatchParamPredicate(object):
+    negatable = True
     def __init__(self, val, config):
         val = as_sorted_tuple(val)
         self.val = val
@@ -258,6 +264,7 @@
         return True
 
 class PhysicalPathPredicate(object):
+    negatable = True
     def __init__(self, val, config):
         if is_nonstr_iter(val):
             self.val = tuple(val)
@@ -276,6 +283,7 @@
         return False
 
 class EffectivePrincipalsPredicate(object):
+    negatable = True
     def __init__(self, val, config):
         if is_nonstr_iter(val):
             self.val = set(val)
diff --git a/pyramid/config/util.py b/pyramid/config/util.py
index a98e71c..a0cbeca 100644
--- a/pyramid/config/util.py
+++ b/pyramid/config/util.py
@@ -110,7 +110,13 @@
                     notted = True
                 pred = predicate_factory(realval, config)
                 if notted:
-                    pred = Notted(pred)
+                    if getattr(pred, 'negatable', False):
+                        pred = Notted(pred)
+                    else:
+                        raise ConfigurationError(
+                            '%s is not a negatable predicate' % (pred.text(),),
+                            )
+
                 hashes = pred.phash()
                 if not is_nonstr_iter(hashes):
                     hashes = [hashes]
diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py
index f6cd414..c813cc5 100644
--- a/pyramid/tests/test_config/test_util.py
+++ b/pyramid/tests/test_config/test_util.py
@@ -365,19 +365,31 @@
         from pyramid.exceptions import ConfigurationError
         self.assertRaises(ConfigurationError, self._callFUT, unknown=1)
 
-    def test_notted(self):
+    def test_notted_configerror(self):
+        from pyramid.exceptions import ConfigurationError
         from pyramid.config import not_
-        from pyramid.testing import DummyRequest
-        request = DummyRequest()
-        _, predicates, _ = self._callFUT(
+        self.assertRaises(ConfigurationError, self._callFUT,
             xhr='xhr',
             request_method=not_('POST'),
             header=not_('header'),
             )
+
+    def test_notted_ok(self):
+        from pyramid.config import not_
+        _, predicates, _ = self._callFUT(
+            xhr='xhr',
+            path_info=not_('/path_info'),
+            header=not_('header'),
+            )
+        from pyramid.testing import DummyRequest
+        request = DummyRequest()
+        request.upath_info = request.path_info
+        request.is_xhr = False
         self.assertEqual(predicates[0].text(), 'xhr = True')
-        self.assertEqual(predicates[1].text(),
-                         "!request_method = POST")
-        self.assertEqual(predicates[2].text(), '!header header')
+        self.assertEqual(predicates[1].text(), '!path_info = /path_info')
+        self.assertEqual(predicates[2].text(),
+                         "!header header")
+        self.assertEqual(predicates[0](None, request), False)
         self.assertEqual(predicates[1](None, request), True)
         self.assertEqual(predicates[2](None, request), True)
         

--
Gitblit v1.9.3