diff options
| -rw-r--r-- | CHANGES.txt | 66 | ||||
| -rw-r--r-- | CONTRIBUTORS.txt | 2 | ||||
| -rw-r--r-- | docs/glossary.rst | 7 | ||||
| -rw-r--r-- | docs/narr/install.rst | 2 | ||||
| -rw-r--r-- | docs/narr/security.rst | 31 | ||||
| -rw-r--r-- | docs/tutorials/wiki2/basiclayout.rst | 2 | ||||
| -rw-r--r-- | docs/tutorials/wiki2/definingviews.rst | 2 | ||||
| -rw-r--r-- | docs/tutorials/wiki2/installation.rst | 7 | ||||
| -rw-r--r-- | pyramid/authentication.py | 20 | ||||
| -rw-r--r-- | pyramid/authorization.py | 3 | ||||
| -rw-r--r-- | pyramid/config/views.py | 26 | ||||
| -rw-r--r-- | pyramid/router.py | 21 | ||||
| -rw-r--r-- | pyramid/tests/fixtures/static/héhé.html | 1 | ||||
| -rw-r--r-- | pyramid/tests/fixtures/static/héhé/index.html | 1 | ||||
| -rw-r--r-- | pyramid/tests/test_authentication.py | 52 | ||||
| -rw-r--r-- | pyramid/tests/test_authorization.py | 9 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_views.py | 7 | ||||
| -rw-r--r-- | pyramid/tests/test_integration.py | 71 | ||||
| -rw-r--r-- | pyramid/tests/test_router.py | 98 | ||||
| -rw-r--r-- | pyramid/tests/test_url.py | 15 |
20 files changed, 394 insertions, 49 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 85dd3be2a..86257cc22 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,69 @@ +next release +============ + +Features +-------- + +- ``ACLAuthorizationPolicy`` supports ``__acl__`` as a callable. This + removes the ambiguity between the potential ``AttributeError`` that would + be raised on the ``context`` when the property was not defined and the + ``AttributeError`` that could be raised from any user-defined code within + a dynamic property. It is recommended to define a dynamic ACL as a callable + to avoid this ambiguity. See https://github.com/Pylons/pyramid/issues/735. + +- Allow a protocol-relative URL (e.g. ``//example.com/images``) to be passed to + ``pyramid.config.Configurator.add_static_view``. This allows + externally-hosted static URLs to be generated based on the current protocol. + +- The ``AuthTktAuthenticationPolicy`` now supports IPv6 addresses when using + the ``include_ip=True`` option. This is possibly incompatible with + alternative ``auth_tkt`` implementations, as the specification does not + define how to properly handle IPv6. See + https://github.com/Pylons/pyramid/issues/831. + +Bug Fixes +--------- + +- 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 to test views found by the super-type of the context. + In the past, only the most specific type containing views would be checked + and if no matching view could be found then a PredicateMismatch would be + raised. Now predicate mismatches don't hide valid views registered on + super-types. Here's an example that now works:: + + .. code-block:: python + + class IResource(Interface): + ... + + @view_config(context=IResource) + def get(context, request): + ... + + @view_config(context=IResource, request_method='POST') + def post(context, request): + ... + + @view_config(context=IResource, request_method='DELETE') + def delete(context, request): + ... + + @implementor(IResource) + class MyResource: + ... + + @view_config(context=MyResource, request_method='POST') + def override_post(context, request): + ... + + Previously the override_post view registration would hide the get + and delete views in the context of MyResource -- leading to a + predicate mismatch error when trying to use GET or DELETE + methods. Now the views are found and no predicate mismatch is + raised. + 1.4 (2012-12-18) ================ diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 971c172f8..02fb81528 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -192,3 +192,5 @@ Contributors - Robert Jackiewicz, 2012/11/12 - John Anderson, 2012/11/14 + +- Bert JW Regeer, 2013/02/01 diff --git a/docs/glossary.rst b/docs/glossary.rst index ccff2d7db..40c15efdc 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -403,10 +403,9 @@ Glossary dispatching and other application configuration tasks. reStructuredText - A `plain text format <http://docutils.sourceforge.net/rst.html>`_ - that is the defacto standard for descriptive text shipped in - :term:`distribution` files, and Python docstrings. This - documentation is authored in ReStructuredText format. + A `plain text markup format <http://docutils.sourceforge.net/rst.html>`_ + that is the defacto standard for documenting Python projects. + The Pyramid documentation is written in reStructuredText. root The object at which :term:`traversal` begins when :app:`Pyramid` diff --git a/docs/narr/install.rst b/docs/narr/install.rst index 04a060ac3..9bc62dc62 100644 --- a/docs/narr/install.rst +++ b/docs/narr/install.rst @@ -269,7 +269,7 @@ you can then create a virtual environment. To do so, invoke the following: .. code-block:: text - $ export $VENV=~/env + $ export VENV=~/env $ virtualenv --no-site-packages $VENV New python executable in /home/foo/env/bin/python Installing setuptools.............done. diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 5b79edd19..203aa2404 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -234,8 +234,8 @@ class: .. code-block:: python :linenos: - from pyramid.security import Everyone from pyramid.security import Allow + from pyramid.security import Everyone class Blog(object): __acl__ = [ @@ -250,8 +250,8 @@ Or, if your resources are persistent, an ACL might be specified via the .. code-block:: python :linenos: - from pyramid.security import Everyone from pyramid.security import Allow + from pyramid.security import Everyone class Blog(object): pass @@ -270,6 +270,27 @@ resource instances with an ACL (as opposed to just decorating their class) in applications such as "CMS" systems where fine-grained access is required on an object-by-object basis. +Dynamic ACLs are also possible by turning the ACL into a callable on the +resource. This may allow the ACL to dynamically generate rules based on +properties of the instance. + +.. code-block:: python + :linenos: + + from pyramid.security import Allow + from pyramid.security import Everyone + + class Blog(object): + def __acl__(self): + return [ + (Allow, Everyone, 'view'), + (Allow, self.owner, 'edit'), + (Allow, 'group:editors', 'edit'), + ] + + def __init__(self, owner): + self.owner = owner + .. index:: single: ACE single: access control entry @@ -282,8 +303,8 @@ Here's an example ACL: .. code-block:: python :linenos: - from pyramid.security import Everyone from pyramid.security import Allow + from pyramid.security import Everyone __acl__ = [ (Allow, Everyone, 'view'), @@ -321,9 +342,9 @@ order dictated by the ACL*. So if you have an ACL like this: .. code-block:: python :linenos: - from pyramid.security import Everyone from pyramid.security import Allow from pyramid.security import Deny + from pyramid.security import Everyone __acl__ = [ (Allow, Everyone, 'view'), @@ -359,8 +380,8 @@ ACE, as below. .. code-block:: python :linenos: - from pyramid.security import Everyone from pyramid.security import Allow + from pyramid.security import Everyone __acl__ = [ (Allow, Everyone, 'view'), diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 68be4ee7c..86fe97956 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -226,7 +226,7 @@ To give a simple example of a model class, we define one named ``MyModel``: :linenos: :language: py -Our example model has an ``__init__`` method that takes a two arguments +Our example model has an ``__init__`` method that takes two arguments (``name``, and ``value``). It stores these values as ``self.name`` and ``self.value`` within the ``__init__`` function itself. The ``MyModel`` class also has a diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 5727816c8..f2ac2f85f 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -126,7 +126,7 @@ The ``view_page`` view function ------------------------------- ``view_page()`` is used to display a single page of our -wiki. It renders the :term:`ReStructuredText` body of a page (stored as +wiki. It renders the :term:`reStructuredText` body of a page (stored as the ``data`` attribute of a ``Page`` model object) as HTML. Then it substitutes an HTML anchor for each *WikiWord* reference in the rendered HTML using a compiled regular expression. diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 255a60ec2..64e069e6f 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -66,6 +66,13 @@ On Windows: startup problems, try putting both the virtualenv and the project into directories that do not contain spaces in their paths. +Pcreate is a script that comes with Pyramid that helps by creating and organizing files +needed as part of a Pyramid project. By passing in `alchemy` was are asking the script to +create the files needed to use SQLAlchemy. By passing in our app name `tutorial` it goes through and +places that application name in all the different files required. For example, the ``initialize_tutorial_db`` +that is in the ``pyramidtut/bin`` directory that we use later in this tutorial was created by `pcreate` + + .. _installing_project_in_dev_mode: diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 4f6ed2c1d..bc0286ed3 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -450,6 +450,12 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): Default: ``False``. Make the requesting IP address part of the authentication data in the cookie. Optional. + For IPv6 this option is not recommended. The ``mod_auth_tkt`` + specification does not specify how to handle IPv6 addresses, so using + this option in combination with IPv6 addresses may cause an + incompatible cookie. It ties the authentication ticket to that + individual's IPv6 address. + ``timeout`` Default: ``None``. Maximum number of seconds which a newly @@ -736,9 +742,17 @@ def calculate_digest(ip, timestamp, secret, userid, tokens, user_data, tokens = bytes_(tokens, 'utf-8') user_data = bytes_(user_data, 'utf-8') hash_obj = hashlib.new(hashalg) - hash_obj.update( - encode_ip_timestamp(ip, timestamp) + secret + userid + b'\0' - + tokens + b'\0' + user_data) + + # Check to see if this is an IPv6 address + if ':' in ip: + ip_timestamp = ip + str(int(timestamp)) + ip_timestamp = bytes_(ip_timestamp) + else: + # encode_ip_timestamp not required, left in for backwards compatibility + ip_timestamp = encode_ip_timestamp(ip, timestamp) + + hash_obj.update(ip_timestamp + secret + userid + b'\0' + + tokens + b'\0' + user_data) digest = hash_obj.hexdigest() hash_obj2 = hashlib.new(hashalg) hash_obj2.update(bytes_(digest) + secret) diff --git a/pyramid/authorization.py b/pyramid/authorization.py index 943f8bd00..1fd05e244 100644 --- a/pyramid/authorization.py +++ b/pyramid/authorization.py @@ -80,6 +80,9 @@ class ACLAuthorizationPolicy(object): except AttributeError: continue + if acl and callable(acl): + acl = acl() + for ace in acl: ace_action, ace_principal, ace_permissions = ace if ace_principal in principals: diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 201ce9899..1c7620e67 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1617,7 +1617,7 @@ class ViewsConfiguratorMixin(object): ): """ Add a default Not Found View to the current configuration state. The view will be called when Pyramid or application code raises an - :exc:`pyramid.httpexceptions.HTTPForbidden` exception (e.g. when a + :exc:`pyramid.httpexceptions.HTTPNotFound` exception (e.g. when a view cannot be found for the request). The simplest example is: .. code-block:: python @@ -1793,6 +1793,10 @@ class ViewsConfiguratorMixin(object): qualified URL (e.g. starts with ``http://`` or similar). In this mode, the ``name`` is used as the prefix of the full URL when generating a URL using :meth:`pyramid.request.Request.static_url`. + Furthermore, if a protocol-relative URL (e.g. ``//example.com/images``) + is used as the ``name`` argument, the generated URL will use the + protocol of the request (http or https, respectively). + For example, if ``add_static_view`` is called like so: .. code-block:: python @@ -1801,20 +1805,14 @@ class ViewsConfiguratorMixin(object): Subsequently, the URLs generated by :meth:`pyramid.request.Request.static_url` for that static view will - be prefixed with ``http://example.com/images``: + be prefixed with ``http://example.com/images`` (the external webserver + listening on ``example.com`` must be itself configured to respond + properly to such a request.): .. code-block:: python static_url('mypackage:images/logo.png', request) - When ``add_static_view`` is called with a ``name`` argument that is - the URL ``http://example.com/images``, subsequent calls to - :meth:`pyramid.request.Request.static_url` with paths that start with - the ``path`` argument passed to ``add_static_view`` will generate a - URL something like ``http://example.com/logo.png``. The external - webserver listening on ``example.com`` must be itself configured to - respond properly to such a request. - See :ref:`static_assets_section` for more information. """ spec = self._make_spec(path) @@ -1858,6 +1856,12 @@ class StaticURLInfo(object): kw['subpath'] = subpath return request.route_url(route_name, **kw) else: + parsed = url_parse(url) + if not parsed.scheme: + # parsed.scheme is readonly, so we have to parse again + # to change the scheme, sigh. + url = urlparse.urlunparse(url_parse( + url, scheme=request.environ['wsgi.url_scheme'])) subpath = url_quote(subpath) return urljoin(url, subpath) @@ -1886,7 +1890,7 @@ class StaticURLInfo(object): # make sure it ends with a slash name = name + '/' - if url_parse(name)[0]: + if url_parse(name).netloc: # it's a URL # url, spec, route_name url = name diff --git a/pyramid/router.py b/pyramid/router.py index 9b6138ea9..63c12a1af 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -1,4 +1,5 @@ from zope.interface import ( + Interface, implementer, providedBy, ) @@ -24,6 +25,7 @@ from pyramid.events import ( NewResponse, ) +from pyramid.exceptions import PredicateMismatch from pyramid.httpexceptions import HTTPNotFound from pyramid.request import Request from pyramid.threadlocal import manager @@ -158,8 +160,23 @@ class Router(object): msg = request.path_info raise HTTPNotFound(msg) else: - response = view_callable(context, request) - + try: + response = view_callable(context, request) + except PredicateMismatch: + # look for other views that meet the predicate + # criteria + for iface in context_iface.flattened(): + view_callable = adapters.lookup( + (IViewClassifier, request.request_iface, iface), + IView, name=view_name, default=None) + if view_callable is not None: + try: + response = view_callable(context, request) + break + except PredicateMismatch: + pass + else: + raise return response def invoke_subrequest(self, request, use_tweens=False): diff --git a/pyramid/tests/fixtures/static/héhé.html b/pyramid/tests/fixtures/static/héhé.html deleted file mode 100644 index fe5e9af50..000000000 --- a/pyramid/tests/fixtures/static/héhé.html +++ /dev/null @@ -1 +0,0 @@ -<html>hehe file</html> diff --git a/pyramid/tests/fixtures/static/héhé/index.html b/pyramid/tests/fixtures/static/héhé/index.html deleted file mode 100644 index 67623d866..000000000 --- a/pyramid/tests/fixtures/static/héhé/index.html +++ /dev/null @@ -1 +0,0 @@ -<html>hehe</html> diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index 123e4f9f5..cfabf9a9d 100644 --- a/pyramid/tests/test_authentication.py +++ b/pyramid/tests/test_authentication.py @@ -561,9 +561,13 @@ class TestAuthTktCookieHelper(unittest.TestCase): helper.BadTicket = auth_tkt.BadTicket return helper - def _makeRequest(self, cookie=None): + def _makeRequest(self, cookie=None, ipv6=False): environ = {'wsgi.version': (1,0)} - environ['REMOTE_ADDR'] = '1.1.1.1' + + if ipv6 is False: + environ['REMOTE_ADDR'] = '1.1.1.1' + else: + environ['REMOTE_ADDR'] = '::1' environ['SERVER_NAME'] = 'localhost' return DummyRequest(environ, cookie=cookie) @@ -612,6 +616,23 @@ class TestAuthTktCookieHelper(unittest.TestCase): self.assertEqual(environ['REMOTE_USER_DATA'],'') self.assertEqual(environ['AUTH_TYPE'],'cookie') + def test_identify_good_cookie_include_ipv6(self): + helper = self._makeOne('secret', include_ip=True) + request = self._makeRequest('ticket', ipv6=True) + result = helper.identify(request) + self.assertEqual(len(result), 4) + self.assertEqual(result['tokens'], ()) + self.assertEqual(result['userid'], 'userid') + self.assertEqual(result['userdata'], '') + self.assertEqual(result['timestamp'], 0) + self.assertEqual(helper.auth_tkt.value, 'ticket') + self.assertEqual(helper.auth_tkt.remote_addr, '::1') + self.assertEqual(helper.auth_tkt.secret, 'secret') + environ = request.environ + self.assertEqual(environ['REMOTE_USER_TOKENS'], ()) + self.assertEqual(environ['REMOTE_USER_DATA'],'') + self.assertEqual(environ['AUTH_TYPE'],'cookie') + def test_identify_good_cookie_dont_include_ip(self): helper = self._makeOne('secret', include_ip=False) request = self._makeRequest('ticket') @@ -1098,6 +1119,20 @@ class TestAuthTicket(unittest.TestCase): self.assertEqual(result, '66f9cc3e423dc57c91df696cf3d1f0d80000000auserid!a,b!') + def test_ipv4(self): + ticket = self._makeOne('secret', 'userid', '198.51.100.1', + time=10, hashalg='sha256') + result = ticket.cookie_value() + self.assertEqual(result, 'b3e7156db4f8abde4439c4a6499a0668f9e7ffd7fa27b'\ + '798400ecdade8d76c530000000auserid!') + + def test_ipv6(self): + ticket = self._makeOne('secret', 'userid', '2001:db8::1', + time=10, hashalg='sha256') + result = ticket.cookie_value() + self.assertEqual(result, 'd025b601a0f12ca6d008aa35ff3a22b7d8f3d1c1456c8'\ + '5becf8760cd7a2fa4910000000auserid!') + class TestBadTicket(unittest.TestCase): def _makeOne(self, msg, expected=None): from pyramid.authentication import BadTicket @@ -1141,6 +1176,19 @@ class Test_parse_ticket(unittest.TestCase): result = self._callFUT('secret', ticket, '0.0.0.0', 'sha512') self.assertEqual(result, (10, 'userid', ['a', 'b'], '')) + def test_ipv4(self): + ticket = 'b3e7156db4f8abde4439c4a6499a0668f9e7ffd7fa27b798400ecdade8d7'\ + '6c530000000auserid!' + result = self._callFUT('secret', ticket, '198.51.100.1', 'sha256') + self.assertEqual(result, (10, 'userid', [''], '')) + + def test_ipv6(self): + ticket = 'd025b601a0f12ca6d008aa35ff3a22b7d8f3d1c1456c85becf8760cd7a2f'\ + 'a4910000000auserid!' + result = self._callFUT('secret', ticket, '2001:db8::1', 'sha256') + self.assertEqual(result, (10, 'userid', [''], '')) + pass + class TestSessionAuthenticationPolicy(unittest.TestCase): def _getTargetClass(self): from pyramid.authentication import SessionAuthenticationPolicy diff --git a/pyramid/tests/test_authorization.py b/pyramid/tests/test_authorization.py index 27f2a18b4..60b1b0c8d 100644 --- a/pyramid/tests/test_authorization.py +++ b/pyramid/tests/test_authorization.py @@ -215,6 +215,15 @@ class TestACLAuthorizationPolicy(unittest.TestCase): result = sorted( policy.principals_allowed_by_permission(context, 'read')) self.assertEqual(result, []) + + def test_callable_acl(self): + from pyramid.security import Allow + context = DummyContext() + fn = lambda self: [(Allow, 'bob', 'read')] + context.__acl__ = fn.__get__(context, context.__class__) + policy = self._makeOne() + result = policy.permits(context, ['bob'], 'read') + self.assertTrue(result) class DummyContext: diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 4cebdce8a..5388001f6 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3737,6 +3737,13 @@ class TestStaticURLInfo(unittest.TestCase): expected = [('http://example.com/', 'anotherpackage:path/', None)] self._assertRegistrations(config, expected) + def test_add_url_noscheme(self): + inst = self._makeOne() + config = self._makeConfig() + inst.add(config, '//example.com', 'anotherpackage:path') + expected = [('//example.com/', 'anotherpackage:path/', None)] + self._assertRegistrations(config, expected) + def test_add_viewname(self): from pyramid.security import NO_PERMISSION_REQUIRED from pyramid.static import static_view diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py index bf3960b2d..c8418c61d 100644 --- a/pyramid/tests/test_integration.py +++ b/pyramid/tests/test_integration.py @@ -3,7 +3,6 @@ import datetime import locale import os -import platform import unittest from pyramid.wsgi import wsgiapp @@ -82,27 +81,40 @@ class TestStaticAppBase(IntegrationBase): res = self.testapp.get('/static/.hiddenfile', status=200) _assertBody(res.body, os.path.join(here, 'fixtures/static/.hiddenfile')) - if defaultlocale is not None and platform.system() == 'Linux': + if defaultlocale is not None: # These tests are expected to fail on LANG=C systems due to decode # errors and on non-Linux systems due to git highchar handling # vagaries def test_highchars_in_pathelement(self): - url = url_quote('/static/héhé/index.html') - res = self.testapp.get(url, status=200) - _assertBody( - res.body, - os.path.join(here, - text_('fixtures/static/héhé/index.html', 'utf-8')) - ) + path = os.path.join( + here, + text_('fixtures/static/héhé/index.html', 'utf-8')) + pathdir = os.path.dirname(path) + body = b'<html>hehe</html>\n' + try: + os.makedirs(pathdir) + with open(path, 'wb') as fp: + fp.write(body) + url = url_quote('/static/héhé/index.html') + res = self.testapp.get(url, status=200) + self.assertEqual(res.body, body) + finally: + os.unlink(path) + os.rmdir(pathdir) def test_highchars_in_filename(self): - url = url_quote('/static/héhé.html') - res = self.testapp.get(url, status=200) - _assertBody( - res.body, - os.path.join(here, - text_('fixtures/static/héhé.html', 'utf-8')) - ) + path = os.path.join( + here, + text_('fixtures/static/héhé.html', 'utf-8')) + body = b'<html>hehe file</html>\n' + with open(path, 'wb') as fp: + fp.write(body) + try: + url = url_quote('/static/héhé.html') + res = self.testapp.get(url, status=200) + self.assertEqual(res.body, body) + finally: + os.unlink(path) def test_not_modified(self): self.testapp.extra_environ = { @@ -634,6 +646,32 @@ class RendererScanAppTest(IntegrationBase, unittest.TestCase): res = testapp.get('/two', status=200) self.assertTrue(b'Two!' in res.body) +class AcceptContentTypeTest(unittest.TestCase): + def setUp(self): + def hello_view(request): + return {'message': 'Hello!'} + from pyramid.config import Configurator + config = Configurator() + config.add_route('hello', '/hello') + config.add_view(hello_view, route_name='hello', accept='text/plain', renderer='string') + config.add_view(hello_view, route_name='hello', accept='application/json', renderer='json') + app = config.make_wsgi_app() + from webtest import TestApp + self.testapp = TestApp(app) + + def test_ordering(self): + res = self.testapp.get('/hello', headers={'Accept': 'application/json; q=1.0, text/plain; q=0.9'}, status=200) + self.assertEqual(res.content_type, 'application/json') + res = self.testapp.get('/hello', headers={'Accept': 'text/plain; q=0.9, application/json; q=1.0'}, status=200) + self.assertEqual(res.content_type, 'application/json') + + def test_wildcards(self): + res = self.testapp.get('/hello', headers={'Accept': 'application/*'}, status=200) + self.assertEqual(res.content_type, 'application/json') + res = self.testapp.get('/hello', headers={'Accept': 'text/*'}, status=200) + self.assertEqual(res.content_type, 'text/plain') + + class DummyContext(object): pass @@ -663,4 +701,3 @@ def _assertBody(body, filename): data = data.replace(b'\r', b'') data = data.replace(b'\n', b'') assert(body == data) - diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index 65152ca05..432959147 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -1164,6 +1164,104 @@ class TestRouter(unittest.TestCase): start_response = DummyStartResponse() self.assertRaises(RuntimeError, router, environ, start_response) + def test_call_view_raises_predicate_mismatch(self): + from pyramid.exceptions import PredicateMismatch + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IRequest + view = DummyView(DummyResponse(), raise_exception=PredicateMismatch) + self._registerView(view, '', IViewClassifier, IRequest, None) + environ = self._makeEnviron() + router = self._makeOne() + start_response = DummyStartResponse() + self.assertRaises(PredicateMismatch, router, environ, start_response) + + def test_call_view_predicate_mismatch_doesnt_hide_views(self): + from pyramid.exceptions import PredicateMismatch + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IRequest, IResponse + from pyramid.response import Response + from zope.interface import Interface, implementer + class IContext(Interface): + pass + @implementer(IContext) + class DummyContext: + pass + context = DummyContext() + self._registerTraverserFactory(context) + view = DummyView(DummyResponse(), raise_exception=PredicateMismatch) + self._registerView(view, '', IViewClassifier, IRequest, + DummyContext) + good_view = DummyView('abc') + self._registerView(self.config.derive_view(good_view), + '', IViewClassifier, IRequest, IContext) + router = self._makeOne() + def make_response(s): + return Response(s) + router.registry.registerAdapter(make_response, (str,), IResponse) + environ = self._makeEnviron() + start_response = DummyStartResponse() + app_iter = router(environ, start_response) + self.assertEqual(app_iter, [b'abc']) + + def test_call_view_multiple_predicate_mismatches_dont_hide_views(self): + from pyramid.exceptions import PredicateMismatch + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IRequest, IResponse + from pyramid.response import Response + from zope.interface import Interface, implementer + class IBaseContext(Interface): + pass + class IContext(IBaseContext): + pass + @implementer(IContext) + class DummyContext: + pass + context = DummyContext() + self._registerTraverserFactory(context) + view1 = DummyView(DummyResponse(), raise_exception=PredicateMismatch) + self._registerView(view1, '', IViewClassifier, IRequest, + DummyContext) + view2 = DummyView(DummyResponse(), raise_exception=PredicateMismatch) + self._registerView(view2, '', IViewClassifier, IRequest, + IContext) + good_view = DummyView('abc') + self._registerView(self.config.derive_view(good_view), + '', IViewClassifier, IRequest, IBaseContext) + router = self._makeOne() + def make_response(s): + return Response(s) + router.registry.registerAdapter(make_response, (str,), IResponse) + environ = self._makeEnviron() + start_response = DummyStartResponse() + app_iter = router(environ, start_response) + self.assertEqual(app_iter, [b'abc']) + + def test_call_view_predicate_mismatch_doesnt_find_unrelated_views(self): + from pyramid.exceptions import PredicateMismatch + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IRequest + from zope.interface import Interface, implementer + class IContext(Interface): + pass + class IOtherContext(Interface): + pass + @implementer(IContext) + class DummyContext: + pass + context = DummyContext() + self._registerTraverserFactory(context) + view = DummyView(DummyResponse(), raise_exception=PredicateMismatch) + self._registerView(view, '', IViewClassifier, IRequest, + DummyContext) + please_dont_call_me_view = DummyView('abc') + self._registerView(self.config.derive_view(please_dont_call_me_view), + '', IViewClassifier, IRequest, IOtherContext) + router = self._makeOne() + environ = self._makeEnviron() + router = self._makeOne() + start_response = DummyStartResponse() + self.assertRaises(PredicateMismatch, router, environ, start_response) + class DummyPredicate(object): def __call__(self, info, request): return True diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py index a7a565356..e33eeebfd 100644 --- a/pyramid/tests/test_url.py +++ b/pyramid/tests/test_url.py @@ -583,6 +583,21 @@ class TestURLMethodsMixin(unittest.TestCase): self.assertEqual(result, 'http://example.com:5432/absstatic/test_url.py') + def test_static_url_noscheme_uses_scheme_from_request(self): + import os + from pyramid.interfaces import IStaticURLInfo + from pyramid.config.views import StaticURLInfo + info = StaticURLInfo() + here = os.path.abspath(os.path.dirname(__file__)) + info.add(self.config, '//subdomain.example.com/static', here) + request = self._makeOne({'wsgi.url_scheme': 'https'}) + registry = request.registry + registry.registerUtility(info, IStaticURLInfo) + abspath = os.path.join(here, 'test_url.py') + result = request.static_url(abspath) + self.assertEqual(result, + 'https://subdomain.example.com/static/test_url.py') + def test_static_path_abspath(self): from pyramid.interfaces import IStaticURLInfo request = self._makeOne() |
