diff options
| author | Chris McDonough <chrism@plope.com> | 2012-10-27 19:45:51 -0400 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2012-10-27 19:45:51 -0400 |
| commit | 220435320613530bde80dd1c4a38a3e719f4af5d (patch) | |
| tree | 7d1b5b1b367b6fc4b5d16cb88e3f8976acf6ece5 | |
| parent | 0a1fb171514f4a41cf8679ef61c06397854dde07 (diff) | |
| parent | 4a6cca62ddf33580b1de210ef5ca54bfb2769243 (diff) | |
| download | pyramid-220435320613530bde80dd1c4a38a3e719f4af5d.tar.gz pyramid-220435320613530bde80dd1c4a38a3e719f4af5d.tar.bz2 pyramid-220435320613530bde80dd1c4a38a3e719f4af5d.zip | |
Merge branch 'master' into 1.4-branch
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | CHANGES.txt | 38 | ||||
| -rw-r--r-- | RELEASING.txt | 6 | ||||
| -rw-r--r-- | docs/api/authentication.rst | 6 | ||||
| -rw-r--r-- | docs/conf.py | 2 | ||||
| -rw-r--r-- | docs/glossary.rst | 22 | ||||
| -rw-r--r-- | docs/narr/viewconfig.rst | 14 | ||||
| -rwxr-xr-x | docs/remake | 2 | ||||
| -rw-r--r-- | docs/whatsnew-1.4.rst | 31 | ||||
| -rw-r--r-- | pyramid/authentication.py | 108 | ||||
| -rw-r--r-- | pyramid/config/predicates.py | 21 | ||||
| -rw-r--r-- | pyramid/config/views.py | 17 | ||||
| -rw-r--r-- | pyramid/tests/test_authentication.py | 100 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_predicates.py | 53 | ||||
| -rw-r--r-- | pyramid/view.py | 2 | ||||
| -rw-r--r-- | setup.py | 2 |
16 files changed, 376 insertions, 50 deletions
diff --git a/.gitignore b/.gitignore index 8e2f83e7d..5fa2a2ee4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.pt.py *.txt.py *~ +.*.swp .coverage .tox/ nosetests.xml @@ -21,3 +22,4 @@ bookenv/ jyenv/ pypyenv/ env*/ +venv/ diff --git a/CHANGES.txt b/CHANGES.txt index fbac16117..1eec21fc2 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,15 +1,5 @@ -Next release -============ - -Features --------- - -- Allow multiple values to be specified to the ``request_param`` view/route - predicate as a sequence. Previously only a single string value was allowed. - See https://github.com/Pylons/pyramid/pull/705 - -- Comments with references to documentation sections placed in scaffold - ``.ini`` files. +1.4a3 (2012-10-26) +================== Bug Fixes --------- @@ -30,13 +20,23 @@ Bug Fixes - When registering a view configuration that named a Chameleon ZPT renderer with a macro name in it (e.g. ``renderer='some/template#somemacro.pt``) as well as a view configuration without a macro name it it that pointed to the - same template (e.g. ``renderer='some/template.pt'), internal caching could + same template (e.g. ``renderer='some/template.pt'``), internal caching could confuse the two, and your code might have rendered one instead of the other. Features -------- +- Allow multiple values to be specified to the ``request_param`` view/route + predicate as a sequence. Previously only a single string value was allowed. + See https://github.com/Pylons/pyramid/pull/705 + +- Comments with references to documentation sections placed in scaffold + ``.ini`` files. + +- Added an HTTP Basic authentication policy + at ``pyramid.authentication.BasicAuthAuthenticationPolicy``. + - The Configurator ``testing_securitypolicy`` method now returns the policy object it creates. @@ -53,6 +53,18 @@ Features ``remembered`` value on the policy, which is the value of the ``principal`` argument it's called with when its ``remember`` method is called. +- New ``physical_path`` view predicate. If specified, this value should be a + string or a tuple representing the physical traversal path of the context + found via traversal for this predicate to match as true. For example: + ``physical_path='/'`` or ``physical_path='/a/b/c'`` or ``physical_path=('', + 'a', 'b', 'c')``. This is not a path prefix match or a regex, it's a + whole-path match. It's useful when you want to always potentially show a + view when some object is traversed to, but you can't be sure about what kind + of object it will be, so you can't use the ``context`` predicate. The + individual path elements inbetween slash characters or in tuple elements + should be the Unicode representation of the name of the resource and should + not be encoded in any way. + 1.4a2 (2012-09-27) ================== diff --git a/RELEASING.txt b/RELEASING.txt index c97c8ef60..379965c53 100644 --- a/RELEASING.txt +++ b/RELEASING.txt @@ -13,10 +13,10 @@ Releasing Pyramid Make sure statement coverage is at 100%:: -- Run Windows tests for Python 2.6, 2.7, and 3.2 if feasible. +- Run Windows tests for Python 2.6, 2.7, 3.2, and 3.3 if feasible. -- Make sure all scaffold tests pass (Py 2.6, 2.7, 3.2 and pypy on UNIX; this - doesn't work on Windows): +- Make sure all scaffold tests pass (Py 2.6, 2.7, 3.2, 3.3 and pypy on UNIX; + this doesn't work on Windows): $ python pyramid/scaffolds/tests.py diff --git a/docs/api/authentication.rst b/docs/api/authentication.rst index 5d4dbd9e3..587026a3b 100644 --- a/docs/api/authentication.rst +++ b/docs/api/authentication.rst @@ -10,12 +10,14 @@ Authentication Policies .. autoclass:: AuthTktAuthenticationPolicy - .. autoclass:: RepozeWho1AuthenticationPolicy - .. autoclass:: RemoteUserAuthenticationPolicy .. autoclass:: SessionAuthenticationPolicy + .. autoclass:: BasicAuthAuthenticationPolicy + + .. autoclass:: RepozeWho1AuthenticationPolicy + Helper Classes ~~~~~~~~~~~~~~ diff --git a/docs/conf.py b/docs/conf.py index 337b1d8bf..9bda4c798 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -81,7 +81,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year # other places throughout the built documents. # # The short X.Y version. -version = '1.4a2' +version = '1.4a3' # The full version, including alpha/beta/rc tags. release = version diff --git a/docs/glossary.rst b/docs/glossary.rst index 96dd826d1..adcf36f7c 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -481,10 +481,24 @@ Glossary :app:`Pyramid` to form a workflow system. virtual root - A resource object representing the "virtual" root of a request; this - is typically the physical root object (the object returned by the - application root factory) unless :ref:`vhosting_chapter` is in - use. + A resource object representing the "virtual" root of a request; this is + typically the :term:`physical root` object unless :ref:`vhosting_chapter` + is in use. + + physical root + The object returned by the application :term:`root factory`. Unlike the + the :term:`virtual root` of a request, it is not impacted by + :ref:`vhosting_chapter`: it will always be the actual object returned by + the root factory, never a subobject. + + physical path + The path required by a traversal which resolve a :term:`resource` starting + from the :term:`physical root`. For example, the physical path of the + ``abc`` subobject of the physical root object is ``/abc``. Physical paths + can also be specified as tuples where the first element is the empty + string (representing the root), and every other element is a Unicode + object, e.g. ``('', 'abc')``. Physical paths are also sometimes called + "traversal paths". lineage An ordered sequence of objects based on a ":term:`location` -aware" diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index 3c7897969..752e6ad72 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -417,6 +417,20 @@ configured view. .. versionadded:: 1.4a2 +``physical_path`` + If specified, this value should be a string or a tuple representing the + :term:`physical path` of the context found via traversal for this predicate + to match as true. For example: ``physical_path='/'`` or + ``physical_path='/a/b/c'`` or ``physical_path=('', 'a', 'b', 'c')``. This is + not a path prefix match or a regex, it's a whole-path match. It's useful + when you want to always potentially show a view when some object is traversed + to, but you can't be sure about what kind of object it will be, so you can't + use the ``context`` predicate. The individual path elements inbetween slash + characters or in tuple elements should be the Unicode representation of the + name of the resource and should not be encoded in any way. + + .. versionadded:: 1.4a3 + ``custom_predicates`` If ``custom_predicates`` is specified, it must be a sequence of references to custom predicate callables. Use custom predicates when no set of diff --git a/docs/remake b/docs/remake index b236f2976..eb818289f 100755 --- a/docs/remake +++ b/docs/remake @@ -1 +1 @@ -make clean html SPHINXBUILD=../env26/bin/sphinx-build +make clean html SPHINXBUILD=../env27/bin/sphinx-build diff --git a/docs/whatsnew-1.4.rst b/docs/whatsnew-1.4.rst index 6aa390e64..59e1f7a96 100644 --- a/docs/whatsnew-1.4.rst +++ b/docs/whatsnew-1.4.rst @@ -165,6 +165,37 @@ Minor Feature Additions - Add ``Base.metadata.bind = engine`` to ``alchemy`` scaffold, so that tables defined imperatively will work. +- Comments with references to documentation sections placed in scaffold + ``.ini`` files. + +- Allow multiple values to be specified to the ``request_param`` view/route + predicate as a sequence. Previously only a single string value was allowed. + See https://github.com/Pylons/pyramid/pull/705 + +- Added an HTTP Basic authentication policy + at :class:`pyramid.authentication.BasicAuthAuthenticationPolicy`. + +- The :meth:`pyramid.config.Configurator.testing_securitypolicy` method now + returns the policy object it creates. + +- The DummySecurityPolicy created by + :meth:`pyramid.config.testing_securitypolicy` now sets a ``forgotten`` value + on the policy (the value ``True``) when its ``forget`` method is called. + + +- The DummySecurityPolicy created by + :meth:`pyramid.config.testing_securitypolicy` now sets a + ``remembered`` value on the policy, which is the value of the ``principal`` + argument it's called with when its ``remember`` method is called. + +- New ``physical_path`` view predicate. If specified, this value should be a + string or a tuple representing the physical traversal path of the context + found via traversal for this predicate to match as true. For example: + ``physical_path='/'`` or ``physical_path='/a/b/c'`` or ``physical_path=('', + 'a', 'b', 'c')``. It's useful when you want to always potentially show a + view when some object is traversed to, but you can't be sure about what kind + of object it will be, so you can't use the ``context`` predicate. + Backwards Incompatibilities --------------------------- diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 83bdb13d1..d4fd7ab8b 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -1,3 +1,4 @@ +import binascii from codecs import utf_8_decode from codecs import utf_8_encode from hashlib import md5 @@ -330,13 +331,13 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): Optional. ``path`` - + Default: ``/``. The path for which the auth_tkt cookie is valid. May be desirable if the application only serves part of a domain. Optional. - + ``http_only`` - + Default: ``False``. Hide cookie from JavaScript by setting the HttpOnly flag. Not honored by all browsers. Optional. @@ -553,7 +554,7 @@ class AuthTktCookieHelper(object): text_type: ('b64unicode', lambda x: b64encode(utf_8_encode(x)[0])), binary_type: ('b64str', lambda x: b64encode(x)), } - + def __init__(self, secret, cookie_name='auth_tkt', secure=False, include_ip=False, timeout=None, reissue_time=None, max_age=None, http_only=False, path="/", wild_domain=True): @@ -632,7 +633,7 @@ class AuthTktCookieHelper(object): remote_addr = environ['REMOTE_ADDR'] else: remote_addr = '0.0.0.0' - + try: timestamp, userid, tokens, user_data = self.parse_ticket( self.secret, cookie, remote_addr) @@ -641,7 +642,7 @@ class AuthTktCookieHelper(object): now = self.now # service tests - if now is None: + if now is None: now = time_mod.time() if self.timeout and ( (timestamp + self.timeout) < now ): @@ -689,7 +690,7 @@ class AuthTktCookieHelper(object): environ = request.environ request._authtkt_reissue_revoked = True return self._get_cookies(environ, '', max_age=EXPIRE) - + def remember(self, request, userid, max_age=None, tokens=()): """ Return a set of Set-Cookie headers; when set into a response, these headers will represent a valid authentication ticket. @@ -783,7 +784,7 @@ class SessionAuthenticationPolicy(CallbackAuthenticationPolicy): Pyramid debug logger about the results of various authentication steps. The output from debugging is useful for reporting to maillist or IRC channels when asking for support. - + """ def __init__(self, prefix='auth.', callback=None, debug=False): @@ -806,3 +807,94 @@ class SessionAuthenticationPolicy(CallbackAuthenticationPolicy): def unauthenticated_userid(self, request): return request.session.get(self.userid_key) + +@implementer(IAuthenticationPolicy) +class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy): + """ A :app:`Pyramid` authentication policy which uses HTTP standard basic + authentication protocol to authenticate users. To use this policy you will + need to provide a callback which checks the supplied user credentials + against your source of login data. + + Constructor Arguments + + ``check`` + + A callback function passed a username, password and request, in that + order as positional arguments. Expected to return ``None`` if the + userid doesn't exist or a sequence of principal identifiers (possibly + empty) if the user does exist. + + ``realm`` + + Default: ``"Realm"``. The Basic Auth Realm string. Usually displayed to + the user by the browser in the login dialog. + + ``debug`` + + Default: ``False``. If ``debug`` is ``True``, log messages to the + Pyramid debug logger about the results of various authentication + steps. The output from debugging is useful for reporting to maillist + or IRC channels when asking for support. + + **Issuing a challenge** + + Regular browsers will not send username/password credentials unless they + first receive a challenge from the server. The following recipe will + register a view that will send a Basic Auth challenge to the user whenever + there is an attempt to call a view which results in a Forbidden response:: + + from pyramid.httpexceptions import HTTPForbidden + from pyramid.httpexceptions import HTTPUnauthorized + from pyramid.security import forget + from pyramid.view import view_config + + @view_config(context=HTTPForbidden) + def basic_challenge(request): + response = HTTPUnauthorized() + response.headers.update(forget(request)) + return response + """ + def __init__(self, check, realm='Realm', debug=False): + self.check = check + self.realm = realm + self.debug = debug + + def unauthenticated_userid(self, request): + credentials = self._get_credentials(request) + if credentials: + return credentials[0] + + def remember(self, request, principal, **kw): + return [] + + def forget(self, request): + return [('WWW-Authenticate', 'Basic realm="%s"' % self.realm)] + + def callback(self, username, request): + # Username arg is ignored. Unfortunately _get_credentials winds up + # getting called twice when authenticated_userid is called. Avoiding + # that, however, winds up duplicating logic from the superclass. + credentials = self._get_credentials(request) + if credentials: + username, password = credentials + return self.check(username, password, request) + + def _get_credentials(self, request): + authorization = request.headers.get('Authorization') + if not authorization: + return None + try: + authmeth, auth = authorization.split(' ', 1) + except ValueError: # not enough values to unpack + return None + if authmeth.lower() != 'basic': + return None + try: + auth = b64decode(auth.strip()).decode('ascii') + except (TypeError, binascii.Error): # can't decode + return None + try: + username, password = auth.split(':', 1) + except ValueError: # not enough values to unpack + return None + return username, password diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py index 100c9454e..adbdcbbc0 100644 --- a/pyramid/config/predicates.py +++ b/pyramid/config/predicates.py @@ -2,15 +2,16 @@ import re from pyramid.exceptions import ConfigurationError +from pyramid.compat import is_nonstr_iter + from pyramid.traversal import ( find_interface, traversal_path, + resource_path_tuple ) from pyramid.urldispatch import _compile_route - from pyramid.util import object_description - from pyramid.session import check_csrf_token from .util import as_sorted_tuple @@ -250,3 +251,19 @@ class CheckCSRFTokenPredicate(object): return self.check_csrf_token(request, val, raises=False) return True +class PhysicalPathPredicate(object): + def __init__(self, val, config): + if is_nonstr_iter(val): + self.val = tuple(val) + else: + val = tuple(filter(None, val.split('/'))) + self.val = ('',) + val + + def text(self): + return 'physical_path = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + return resource_path_tuple(context) == self.val + diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 15263ad04..e52f9d64b 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1014,6 +1014,22 @@ class ViewsConfiguratorMixin(object): .. versionadded:: 1.4a2 + physical_path + + If specified, this value should be a string or a tuple representing + the :term:`physical path` of the context found via traversal for this + predicate to match as true. For example: ``physical_path='/'`` or + ``physical_path='/a/b/c'`` or ``physical_path=('', 'a', 'b', 'c')``. + This is not a path prefix match or a regex, it's a whole-path match. + It's useful when you want to always potentially show a view when some + object is traversed to, but you can't be sure about what kind of + object it will be, so you can't use the ``context`` predicate. The + individual path elements inbetween slash characters or in tuple + elements should be the Unicode representation of the name of the + resource and should not be encoded in any way. + + .. versionadded:: 1.4a3 + custom_predicates This value should be a sequence of references to custom @@ -1370,6 +1386,7 @@ class ViewsConfiguratorMixin(object): ('request_type', p.RequestTypePredicate), ('match_param', p.MatchParamPredicate), ('check_csrf', p.CheckCSRFTokenPredicate), + ('physical_path', p.PhysicalPathPredicate), ('custom', p.CustomPredicate), ): self.add_view_predicate(name, factory) diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index e513b9a48..dfe3cf0b0 100644 --- a/pyramid/tests/test_authentication.py +++ b/pyramid/tests/test_authentication.py @@ -14,7 +14,7 @@ class TestCallbackAuthenticationPolicyDebugging(unittest.TestCase): def tearDown(self): del self.config - + def debug(self, msg): self.messages.append(msg) @@ -151,7 +151,7 @@ class TestRepozeWho1AuthenticationPolicy(unittest.TestCase): def _makeOne(self, identifier_name='auth_tkt', callback=None): return self._getTargetClass()(identifier_name, callback) - + def test_class_implements_IAuthenticationPolicy(self): from zope.interface.verify import verifyClass from pyramid.interfaces import IAuthenticationPolicy @@ -251,7 +251,7 @@ class TestRepozeWho1AuthenticationPolicy(unittest.TestCase): result = policy.remember(request, 'fred') self.assertEqual(result[0], request.environ) self.assertEqual(result[1], {'repoze.who.userid':'fred'}) - + def test_forget_no_plugins(self): request = DummyRequest({}) policy = self._makeOne() @@ -276,7 +276,7 @@ class TestRemoteUserAuthenticationPolicy(unittest.TestCase): def _makeOne(self, environ_key='REMOTE_USER', callback=None): return self._getTargetClass()(environ_key, callback) - + def test_class_implements_IAuthenticationPolicy(self): from zope.interface.verify import verifyClass from pyramid.interfaces import IAuthenticationPolicy @@ -301,7 +301,7 @@ class TestRemoteUserAuthenticationPolicy(unittest.TestCase): request = DummyRequest({}) policy = self._makeOne() self.assertEqual(policy.authenticated_userid(request), None) - + def test_authenticated_userid(self): request = DummyRequest({'REMOTE_USER':'fred'}) policy = self._makeOne() @@ -326,7 +326,7 @@ class TestRemoteUserAuthenticationPolicy(unittest.TestCase): policy = self._makeOne() result = policy.remember(request, 'fred') self.assertEqual(result, []) - + def test_forget(self): request = DummyRequest({'REMOTE_USER':'fred'}) policy = self._makeOne() @@ -375,7 +375,7 @@ class TestAutkTktAuthenticationPolicy(unittest.TestCase): request = DummyRequest({}) policy = self._makeOne(None, None) self.assertEqual(policy.authenticated_userid(request), None) - + def test_authenticated_userid_callback_returns_None(self): request = DummyRequest({}) def callback(userid, request): @@ -426,7 +426,7 @@ class TestAutkTktAuthenticationPolicy(unittest.TestCase): result = policy.remember(request, 'fred', a=1, b=2) self.assertEqual(policy.cookie.kw, {'a':1, 'b':2}) self.assertEqual(result, []) - + def test_forget(self): request = DummyRequest({}) policy = self._makeOne(None, None) @@ -482,7 +482,7 @@ class TestAuthTktCookieHelper(unittest.TestCase): request = self._makeRequest(None) result = helper.identify(request) self.assertEqual(result, None) - + def test_identify_good_cookie_include_ip(self): helper = self._makeOne('secret', include_ip=True) request = self._makeRequest('ticket') @@ -605,7 +605,7 @@ class TestAuthTktCookieHelper(unittest.TestCase): request = self._makeRequest('ticket') result = helper.identify(request) self.assertEqual(result, None) - + def test_identify_cookie_timed_out(self): helper = self._makeOne('secret', timeout=1) request = self._makeRequest({'HTTP_COOKIE':'auth_tkt=bogus'}) @@ -828,7 +828,7 @@ class TestAuthTktCookieHelper(unittest.TestCase): self.assertEqual(result[1][0], 'Set-Cookie') self.assertTrue(result[1][1].endswith('; Path=/; Domain=example.com')) self.assertTrue(result[1][1].startswith('auth_tkt=')) - + def test_remember_binary_userid(self): import base64 helper = self._makeOne('secret') @@ -1106,6 +1106,78 @@ class TestSessionAuthenticationPolicy(unittest.TestCase): self.assertEqual(request.session.get('userid'), None) self.assertEqual(result, []) +class TestBasicAuthAuthenticationPolicy(unittest.TestCase): + def _getTargetClass(self): + from pyramid.authentication import BasicAuthAuthenticationPolicy as cls + return cls + + def _makeOne(self, check): + return self._getTargetClass()(check, realm='SomeRealm') + + def test_class_implements_IAuthenticationPolicy(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IAuthenticationPolicy + verifyClass(IAuthenticationPolicy, self._getTargetClass()) + + def test_unauthenticated_userid(self): + import base64 + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic %s' % base64.b64encode( + bytes_('chrisr:password')).decode('ascii') + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), 'chrisr') + + def test_unauthenticated_userid_no_credentials(self): + request = testing.DummyRequest() + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_unauthenticated_bad_header(self): + request = testing.DummyRequest() + request.headers['Authorization'] = '...' + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_unauthenticated_userid_not_basic(self): + request = testing.DummyRequest() + request.headers['Authorization'] = 'Complicated things' + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_unauthenticated_userid_corrupt_base64(self): + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic chrisr:password' + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_authenticated_userid(self): + import base64 + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic %s' % base64.b64encode( + bytes_('chrisr:password')).decode('ascii') + def check(username, password, request): + return [] + policy = self._makeOne(check) + self.assertEqual(policy.authenticated_userid(request), 'chrisr') + + def test_unauthenticated_userid_invalid_payload(self): + import base64 + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic %s' % base64.b64encode( + bytes_('chrisrpassword')).decode('ascii') + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_remember(self): + policy = self._makeOne(None) + self.assertEqual(policy.remember(None, None), []) + + def test_forget(self): + policy = self._makeOne(None) + self.assertEqual(policy.forget(None), [ + ('WWW-Authenticate', 'Basic realm="SomeRealm"')]) + + class DummyContext: pass @@ -1130,7 +1202,7 @@ class DummyRequest: class DummyWhoPlugin: def remember(self, environ, identity): return environ, identity - + def forget(self, environ, identity): return environ, identity @@ -1164,7 +1236,7 @@ class DummyAuthTktModule(object): raise self.BadTicket() return self.timestamp, self.userid, self.tokens, self.user_data self.parse_ticket = parse_ticket - + class AuthTicket(object): def __init__(self, secret, userid, remote_addr, **kw): self.secret = secret @@ -1186,4 +1258,4 @@ class DummyAuthTktModule(object): class DummyResponse: def __init__(self): self.headerlist = [] - + diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py index 2f0ef4132..84d9b184d 100644 --- a/pyramid/tests/test_config/test_predicates.py +++ b/pyramid/tests/test_config/test_predicates.py @@ -381,6 +381,59 @@ class TestHeaderPredicate(unittest.TestCase): inst = self._makeOne(r'abc:\d+') self.assertEqual(inst.phash(), r'header abc=\d+') +class Test_PhysicalPathPredicate(unittest.TestCase): + def _makeOne(self, val, config): + from pyramid.config.predicates import PhysicalPathPredicate + return PhysicalPathPredicate(val, config) + + def test_text(self): + inst = self._makeOne('/', None) + self.assertEqual(inst.text(), "physical_path = ('',)") + + def test_phash(self): + inst = self._makeOne('/', None) + self.assertEqual(inst.phash(), "physical_path = ('',)") + + def test_it_call_val_tuple_True(self): + inst = self._makeOne(('', 'abc'), None) + root = Dummy() + root.__name__ = '' + root.__parent__ = None + context = Dummy() + context.__name__ = 'abc' + context.__parent__ = root + self.assertTrue(inst(context, None)) + + def test_it_call_val_list_True(self): + inst = self._makeOne(['', 'abc'], None) + root = Dummy() + root.__name__ = '' + root.__parent__ = None + context = Dummy() + context.__name__ = 'abc' + context.__parent__ = root + self.assertTrue(inst(context, None)) + + def test_it_call_val_str_True(self): + inst = self._makeOne('/abc', None) + root = Dummy() + root.__name__ = '' + root.__parent__ = None + context = Dummy() + context.__name__ = 'abc' + context.__parent__ = root + self.assertTrue(inst(context, None)) + + def test_it_call_False(self): + inst = self._makeOne('/', None) + root = Dummy() + root.__name__ = '' + root.__parent__ = None + context = Dummy() + context.__name__ = 'abc' + context.__parent__ = root + self.assertFalse(inst(context, None)) + class predicate(object): def __repr__(self): return 'predicate' diff --git a/pyramid/view.py b/pyramid/view.py index 76f466b83..51ded423c 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -170,7 +170,7 @@ class view_config(object): ``request_type``, ``route_name``, ``request_method``, ``request_param``, ``containment``, ``xhr``, ``accept``, ``header``, ``path_info``, ``custom_predicates``, ``decorator``, ``mapper``, ``http_cache``, - ``match_param``, ``csrf_token``, and ``predicates``. + ``match_param``, ``csrf_token``, ``physical_path``, and ``predicates``. The meanings of these arguments are the same as the arguments passed to :meth:`pyramid.config.Configurator.add_view`. If any argument is left @@ -68,7 +68,7 @@ testing_extras = tests_require + [ ] setup(name='pyramid', - version='1.4a2', + version='1.4a3', description=('The Pyramid web application development framework, a ' 'Pylons project'), long_description=README + '\n\n' + CHANGES, |
