summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt66
-rw-r--r--CONTRIBUTORS.txt2
-rw-r--r--docs/glossary.rst7
-rw-r--r--docs/narr/install.rst2
-rw-r--r--docs/narr/security.rst31
-rw-r--r--docs/tutorials/wiki2/basiclayout.rst2
-rw-r--r--docs/tutorials/wiki2/definingviews.rst2
-rw-r--r--docs/tutorials/wiki2/installation.rst7
-rw-r--r--pyramid/authentication.py20
-rw-r--r--pyramid/authorization.py3
-rw-r--r--pyramid/config/views.py26
-rw-r--r--pyramid/router.py21
-rw-r--r--pyramid/tests/fixtures/static/héhé.html1
-rw-r--r--pyramid/tests/fixtures/static/héhé/index.html1
-rw-r--r--pyramid/tests/test_authentication.py52
-rw-r--r--pyramid/tests/test_authorization.py9
-rw-r--r--pyramid/tests/test_config/test_views.py7
-rw-r--r--pyramid/tests/test_integration.py71
-rw-r--r--pyramid/tests/test_router.py98
-rw-r--r--pyramid/tests/test_url.py15
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()