Skip to content

Commit

Permalink
add not_ predicate feature
Browse files Browse the repository at this point in the history
  • Loading branch information
mcdonc committed Jul 24, 2013
1 parent d79c033 commit 32333e4
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 2 deletions.
24 changes: 24 additions & 0 deletions CHANGES.txt
Expand Up @@ -4,6 +4,30 @@ Next Release
Features
--------

- Add the ability to invert the result of any view, route, or subscriber
predicate using the ``not_`` class. For example::

from pyramid.config import not_

@view_config(route_name='myroute', request_method=not_('POST'))
def myview(request): ...

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).

The :class:`pyramid.config.not_` class can be used against any value that is
a predicate value passed in any of these contexts:

- ``pyramid.config.Configurator.add_view``

- ``pyramid.config.Configurator.add_route``

- ``pyramid.config.Configurator.add_subscriber``

- ``pyramid.view.view_config``

- ``pyramid.events.subscriber``

- ``scripts/prequest.py``: add support for submitting ``PUT`` and ``PATCH``
requests. See https://github.com/Pylons/pyramid/pull/1033. add support for
submitting ``OPTIONS`` and ``PROPFIND`` requests, and allow users to specify
Expand Down
1 change: 1 addition & 0 deletions docs/api/config.rst
Expand Up @@ -135,3 +135,4 @@
will only exist for the lifetime of the actual applications for which they
are being used.

.. autoclass:: not_
27 changes: 27 additions & 0 deletions docs/narr/viewconfig.rst
Expand Up @@ -557,6 +557,33 @@ form of :term:`declarative configuration`, while
:meth:`pyramid.config.Configurator.add_view` is a form of :term:`imperative
configuration`. However, they both do the same thing.

Inverting Predicate Values
~~~~~~~~~~~~~~~~~~~~~~~~~~

You can invert the meaning of any predicate value by wrapping it in a call to
:class:`pyramid.config.not_`.

.. code-block:: python
:linenos:
from pyramid.config import not_
config.add_view(
'mypackage.views.my_view',
route_name='ok',
request_method=not_('POST')
)
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.

This technique of wrapping a predicate value in ``not_`` can be used anywhere
predicate values are accepted:

- :meth:`pyramid.config.Configurator.add_view`

- :meth:`pyramid.view.view_config`

.. index::
single: view_config placement

Expand Down
26 changes: 26 additions & 0 deletions docs/whatsnew-1.5.rst
Expand Up @@ -12,6 +12,32 @@ Feature Additions

The feature additions in Pyramid 1.5 follow.

- Add the ability to invert the result of any view, route, or subscriber
predicate value using the ``not_`` class. For example:

.. code-block:: python
from pyramid.config import not_
@view_config(route_name='myroute', request_method=not_('POST'))
def myview(request): ...
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.

The :class:`pyramid.config.not_` class can be used against any value that is
a predicate value passed in any of these contexts:

- :meth:`pyramid.config.Configurator.add_view`

- :meth:`pyramid.config.Configurator.add_route`

- :meth:`pyramid.config.Configurator.add_subscriber`

- :meth:`pyramid.view.view_config`

- :meth:`pyramid.events.subscriber`

- View lookup will now search for valid views based on the inheritance
hierarchy of the context. It tries to find views based on the most specific
context first, and upon predicate failure, will move up the inheritance chain
Expand Down
5 changes: 4 additions & 1 deletion pyramid/config/__init__.py
Expand Up @@ -70,7 +70,7 @@
from pyramid.config.settings import SettingsConfiguratorMixin
from pyramid.config.testing import TestingConfiguratorMixin
from pyramid.config.tweens import TweensConfiguratorMixin
from pyramid.config.util import PredicateList
from pyramid.config.util import PredicateList, not_
from pyramid.config.views import ViewsConfiguratorMixin
from pyramid.config.zca import ZCAConfiguratorMixin

Expand All @@ -86,6 +86,9 @@

ConfigurationError = ConfigurationError # pyflakes

not_ = not_ # pyflakes, this is an API


class Configurator(
TestingConfiguratorMixin,
TweensConfiguratorMixin,
Expand Down
1 change: 1 addition & 0 deletions pyramid/config/predicates.py
Expand Up @@ -294,3 +294,4 @@ def __call__(self, context, request):
if self.val.issubset(rpset):
return True
return False

38 changes: 37 additions & 1 deletion pyramid/config/util.py
Expand Up @@ -28,6 +28,35 @@ def as_sorted_tuple(val):
val = tuple(sorted(val))
return val

class not_(object):
def __init__(self, value):
self.value = value

class Notted(object):
def __init__(self, predicate):
self.predicate = predicate

def _notted_text(self, val):
# if the underlying predicate doesnt return a value, it's not really
# a predicate, it's just something pretending to be a predicate,
# so dont update the hash
if val:
val = '!' + val
return val

def text(self):
return self._notted_text(self.predicate.text())

def phash(self):
return self._notted_text(self.predicate.phash())

def __call__(self, context, request):
result = self.predicate(context, request)
phash = self.phash()
if phash:
result = not result
return result

# under = after
# over = before

Expand Down Expand Up @@ -74,7 +103,14 @@ def make(self, config, **kw):
if not isinstance(vals, predvalseq):
vals = (vals,)
for val in vals:
pred = predicate_factory(val, config)
realval = val
notted = False
if isinstance(val, not_):
realval = val.value
notted = True
pred = predicate_factory(realval, config)
if notted:
pred = Notted(pred)
hashes = pred.phash()
if not is_nonstr_iter(hashes):
hashes = [hashes]
Expand Down
47 changes: 47 additions & 0 deletions pyramid/tests/test_config/test_util.py
Expand Up @@ -364,6 +364,23 @@ def test_request_method_ordering_hashes_same(self):
def test_unknown_predicate(self):
from pyramid.exceptions import ConfigurationError
self.assertRaises(ConfigurationError, self._callFUT, unknown=1)

def test_notted(self):
from pyramid.config import not_
from pyramid.testing import DummyRequest
request = DummyRequest()
_, predicates, _ = self._callFUT(
xhr='xhr',
request_method=not_('POST'),
header=not_('header'),
)
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](None, request), True)
self.assertEqual(predicates[2](None, request), True)


class Test_takes_one_arg(unittest.TestCase):
def _callFUT(self, view, attr=None, argname=None):
Expand Down Expand Up @@ -551,7 +568,37 @@ def method(self, request):
foo = Foo()
self.assertTrue(self._callFUT(foo.method))

class TestNotted(unittest.TestCase):
def _makeOne(self, predicate):
from pyramid.config.util import Notted
return Notted(predicate)

def test_it_with_phash_val(self):
pred = DummyPredicate('val')
inst = self._makeOne(pred)
self.assertEqual(inst.text(), '!val')
self.assertEqual(inst.phash(), '!val')
self.assertEqual(inst(None, None), False)

def test_it_without_phash_val(self):
pred = DummyPredicate('')
inst = self._makeOne(pred)
self.assertEqual(inst.text(), '')
self.assertEqual(inst.phash(), '')
self.assertEqual(inst(None, None), True)

class DummyPredicate(object):
def __init__(self, result):
self.result = result

def text(self):
return self.result

phash = text

def __call__(self, context, request):
return True

class DummyCustomPredicate(object):
def __init__(self):
self.__text__ = 'custom predicate'
Expand Down

2 comments on commit 32333e4

@tshepang
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find not_ uglier than is_not. The latter is also clearer, even if a bit. Too late to change?

@mcdonc
Copy link
Member Author

@mcdonc mcdonc commented on 32333e4 Jul 28, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH I'd rather leave it the way it is. You'll barely notice after a while. ;-)

Please sign in to comment.