summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Merickel <michael@merickel.org>2014-11-17 02:18:15 -0600
committerMichael Merickel <michael@merickel.org>2014-11-17 02:18:15 -0600
commit0c5e5ac9bbb9d0e1bc4aa8f3497f2c7f54ca96d3 (patch)
treeaf1f326e83b8e799abe17bad6f1259185b52f813
parent823ac447329e46e5826e8e3228d9f847f9790ee8 (diff)
parentc0f1fc8d31df45371d5ae6689d3e0a39c058c3ac (diff)
downloadpyramid-0c5e5ac9bbb9d0e1bc4aa8f3497f2c7f54ca96d3.tar.gz
pyramid-0c5e5ac9bbb9d0e1bc4aa8f3497f2c7f54ca96d3.tar.bz2
pyramid-0c5e5ac9bbb9d0e1bc4aa8f3497f2c7f54ca96d3.zip
Merge branch 'master' into feature.pshell-pythonstartup
-rw-r--r--.travis.yml22
-rw-r--r--CHANGES.txt24
-rw-r--r--CONTRIBUTORS.txt2
-rw-r--r--docs/narr/i18n.rst2
-rw-r--r--pyramid/config/views.py21
-rw-r--r--pyramid/scripts/proutes.py68
-rw-r--r--pyramid/tests/test_config/test_views.py37
-rw-r--r--pyramid/tests/test_response.py11
-rw-r--r--pyramid/tests/test_scripts/test_proutes.py93
-rw-r--r--pyramid/tests/test_util.py43
-rw-r--r--pyramid/util.py32
-rw-r--r--tox.ini2
12 files changed, 311 insertions, 46 deletions
diff --git a/.travis.yml b/.travis.yml
index 4ca998c42..4ff4939d9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,18 +1,20 @@
# Wire up travis
language: python
-python:
- - 2.6
- - 2.7
- - pypy
- - 3.2
- - 3.3
- - 3.4
- - pypy3
+env:
+ - TOXENV=py26
+ - TOXENV=py27
+ - TOXENV=py32
+ - TOXENV=py33
+ - TOXENV=py34
+ - TOXENV=pypy
+ - TOXENV=cover
-install: python setup.py dev
+install:
+ - travis_retry pip install tox
-script: python setup.py test -q
+script:
+ - travis_retry tox
notifications:
email:
diff --git a/CHANGES.txt b/CHANGES.txt
index f72a793a5..590a6db0e 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -12,8 +12,8 @@ Features
parameter to assist with includeable packages that wish to resolve
resources relative to the package in which the ``Configurator`` was created.
This is especially useful for addons that need to load asset specs from
- settings, in which case it is natural for a user to define things relative
- to their own packages.
+ settings, in which case it is may be natural for a developer to define
+ imports or assets relative to the top-level package.
See https://github.com/Pylons/pyramid/pull/1337
- Added line numbers to the log formatters in the scaffolds to assist with
@@ -28,6 +28,20 @@ Features
defined in the environment prior to launching the interpreter.
See https://github.com/Pylons/pyramid/pull/1448
+- Make it simple to define notfound and forbidden views that wish to use
+ the default exception-response view but with altered predicates and other
+ configuration options. The ``view`` argument is now optional in
+ ``config.add_notfound_view`` and ``config.add_forbidden_view``..
+ See https://github.com/Pylons/pyramid/issues/494
+
+- Greatly improve the readability of the ``pcreate`` shell script output.
+ See https://github.com/Pylons/pyramid/pull/1453
+
+- Improve robustness to timing attacks in the ``AuthTktCookieHelper`` and
+ the ``SignedCookieSessionFactory`` classes by using the stdlib's
+ ``hmac.compare_digest`` if it is available (such as Python 2.7.7+ and 3.3+).
+ See https://github.com/Pylons/pyramid/pull/1457
+
Bug Fixes
---------
@@ -62,6 +76,12 @@ Bug Fixes
add another callback to the list. See
https://github.com/Pylons/pyramid/pull/1373
+- Fix a failing unittest caused by differing mimetypes across various OSs.
+ See https://github.com/Pylons/pyramid/issues/1405
+
+- Fix route generation for static view asset specifications having no path.
+ See https://github.com/Pylons/pyramid/pull/1377
+
Docs
----
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index c77d3e92c..66f029cb7 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -232,3 +232,5 @@ Contributors
- Amit Mane, 2014/01/23
- Fenton Travers, 2014/05/06
+
+- Randall Leeds, 2014/11/11
diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst
index 3313f8dad..3c804a158 100644
--- a/docs/narr/i18n.rst
+++ b/docs/narr/i18n.rst
@@ -354,7 +354,7 @@ command from Gettext:
$ mkdir -p es/LC_MESSAGES
$ msginit -l es -o es/LC_MESSAGES/myapplication.po
-This will create a new the message catalog ``.po`` file will in:
+This will create a new message catalog ``.po`` file in:
``myapplication/locale/es/LC_MESSAGES/myapplication.po``.
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index 5ca696069..c01b72e12 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -53,6 +53,7 @@ from pyramid.exceptions import (
from pyramid.httpexceptions import (
HTTPForbidden,
HTTPNotFound,
+ default_exceptionresponse_view,
)
from pyramid.registry import (
@@ -1185,10 +1186,6 @@ class ViewsConfiguratorMixin(object):
predlist = self.get_predlist('view')
def register(permission=permission, renderer=renderer):
- # the discrim_func above is guaranteed to have been called already
- order = view_intr['order']
- preds = view_intr['predicates']
- phash = view_intr['phash']
request_iface = IRequest
if route_name is not None:
request_iface = self.registry.queryUtility(IRouteRequest,
@@ -1591,9 +1588,12 @@ class ViewsConfiguratorMixin(object):
config.add_forbidden_view(forbidden)
+ If ``view`` argument is not provided, the view callable defaults to
+ :func:`~pyramid.httpexceptions.default_exceptionresponse_view`.
+
All arguments have the same meaning as
:meth:`pyramid.config.Configurator.add_view` and each predicate
- argument restricts the set of circumstances under which this notfound
+ argument restricts the set of circumstances under which this forbidden
view will be invoked. Unlike
:meth:`pyramid.config.Configurator.add_view`, this method will raise
an exception if passed ``name``, ``permission``, ``context``,
@@ -1609,6 +1609,9 @@ class ViewsConfiguratorMixin(object):
% arg
)
+ if view is None:
+ view = default_exceptionresponse_view
+
settings = dict(
view=view,
context=HTTPForbidden,
@@ -1671,6 +1674,9 @@ class ViewsConfiguratorMixin(object):
config.add_notfound_view(notfound)
+ If ``view`` argument is not provided, the view callable defaults to
+ :func:`~pyramid.httpexceptions.default_exceptionresponse_view`.
+
All arguments except ``append_slash`` have the same meaning as
:meth:`pyramid.config.Configurator.add_view` and each predicate
argument restricts the set of circumstances under which this notfound
@@ -1697,6 +1703,9 @@ class ViewsConfiguratorMixin(object):
% arg
)
+ if view is None:
+ view = default_exceptionresponse_view
+
settings = dict(
view=view,
context=HTTPNotFound,
@@ -1942,7 +1951,7 @@ class StaticURLInfo(object):
sep = os.sep
else:
sep = '/'
- if not spec.endswith(sep):
+ if not spec.endswith(sep) and not spec.endswith(':'):
spec = spec + sep
# we also make sure the name ends with a slash, purely as a
diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py
index 5784026bb..d0c1aa13e 100644
--- a/pyramid/scripts/proutes.py
+++ b/pyramid/scripts/proutes.py
@@ -5,10 +5,15 @@ import textwrap
from pyramid.paster import bootstrap
from pyramid.scripts.common import parse_vars
+
+PAD = 3
+
+
def main(argv=sys.argv, quiet=False):
command = PRoutesCommand(argv, quiet)
return command.run()
+
class PRoutesCommand(object):
description = """\
Print all URL dispatch routes used by a Pyramid application in the
@@ -43,7 +48,7 @@ class PRoutesCommand(object):
def out(self, msg): # pragma: no cover
if not self.quiet:
print(msg)
-
+
def run(self, quiet=False):
if not self.args:
self.out('requires a config file argument')
@@ -52,6 +57,8 @@ class PRoutesCommand(object):
from pyramid.interfaces import IRouteRequest
from pyramid.interfaces import IViewClassifier
from pyramid.interfaces import IView
+ from pyramid.interfaces import IMultiView
+
from zope.interface import Interface
config_uri = self.args[0]
@@ -59,13 +66,23 @@ class PRoutesCommand(object):
registry = env['registry']
mapper = self._get_mapper(registry)
if mapper is not None:
+ mapped_routes = [('Name', 'Pattern', 'View')]
+
+ max_name = len('Name')
+ max_pattern = len('Pattern')
+ max_view = len('View')
+
routes = mapper.get_routes()
- fmt = '%-15s %-30s %-25s'
+
if not routes:
return 0
- self.out(fmt % ('Name', 'Pattern', 'View'))
- self.out(
- fmt % ('-'*len('Name'), '-'*len('Pattern'), '-'*len('View')))
+
+ mapped_routes.append((
+ '-' * max_name,
+ '-' * max_pattern,
+ '-' * max_view,
+ ))
+
for route in routes:
pattern = route.pattern
if not pattern.startswith('/'):
@@ -73,13 +90,50 @@ class PRoutesCommand(object):
request_iface = registry.queryUtility(IRouteRequest,
name=route.name)
view_callable = None
+
if (request_iface is None) or (route.factory is not None):
- self.out(fmt % (route.name, pattern, '<unknown>'))
+ view_callable = '<unknown>'
else:
view_callable = registry.adapters.lookup(
(IViewClassifier, request_iface, Interface),
IView, name='', default=None)
- self.out(fmt % (route.name, pattern, view_callable))
+
+ if view_callable is not None:
+ if IMultiView.providedBy(view_callable):
+ view_callables = [
+ x[1] for x in view_callable.views
+ ]
+ else:
+ view_callables = [view_callable]
+
+ for view_func in view_callables:
+ view_callable = '%s.%s' % (
+ view_func.__module__,
+ view_func.__name__,
+ )
+ else:
+ view_callable = str(None)
+
+ if len(route.name) > max_name:
+ max_name = len(route.name)
+
+ if len(pattern) > max_pattern:
+ max_pattern = len(pattern)
+
+ if len(view_callable) > max_view:
+ max_view = len(view_callable)
+
+ mapped_routes.append((route.name, pattern, view_callable))
+
+ fmt = '%-{0}s %-{1}s %-{2}s'.format(
+ max_name + PAD,
+ max_pattern + PAD,
+ max_view + PAD,
+ )
+
+ for route_data in mapped_routes:
+ self.out(fmt % route_data)
+
return 0
if __name__ == '__main__': # pragma: no cover
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index a0d9ee0c3..b0d03fb72 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -1783,6 +1783,21 @@ class TestViewsConfigurationMixin(unittest.TestCase):
result = view(None, request)
self.assertEqual(result, 'OK')
+ def test_add_forbidden_view_no_view_argument(self):
+ from zope.interface import implementedBy
+ from pyramid.interfaces import IRequest
+ from pyramid.httpexceptions import HTTPForbidden
+ config = self._makeOne(autocommit=True)
+ config.setup_registry()
+ config.add_forbidden_view()
+ request = self._makeRequest(config)
+ view = self._getViewCallable(config,
+ ctx_iface=implementedBy(HTTPForbidden),
+ request_iface=IRequest)
+ context = HTTPForbidden()
+ result = view(context, request)
+ self.assertEqual(result, context)
+
def test_add_forbidden_view_allows_other_predicates(self):
from pyramid.renderers import null_renderer
config = self._makeOne(autocommit=True)
@@ -1860,6 +1875,21 @@ class TestViewsConfigurationMixin(unittest.TestCase):
result = view(None, request)
self.assertEqual(result, (None, request))
+ def test_add_notfound_view_no_view_argument(self):
+ from zope.interface import implementedBy
+ from pyramid.interfaces import IRequest
+ from pyramid.httpexceptions import HTTPNotFound
+ config = self._makeOne(autocommit=True)
+ config.setup_registry()
+ config.add_notfound_view()
+ request = self._makeRequest(config)
+ view = self._getViewCallable(config,
+ ctx_iface=implementedBy(HTTPNotFound),
+ request_iface=IRequest)
+ context = HTTPNotFound()
+ result = view(context, request)
+ self.assertEqual(result, context)
+
def test_add_notfound_view_allows_other_predicates(self):
from pyramid.renderers import null_renderer
config = self._makeOne(autocommit=True)
@@ -3868,6 +3898,13 @@ class TestStaticURLInfo(unittest.TestCase):
('http://example.com/', 'anotherpackage:path/', None, None)]
self._assertRegistrations(config, expected)
+ def test_add_package_root(self):
+ inst = self._makeOne()
+ config = self._makeConfig()
+ inst.add(config, 'http://example.com', 'package:')
+ expected = [('http://example.com/', 'package:', None, None)]
+ self._assertRegistrations(config, expected)
+
def test_add_url_withendslash(self):
inst = self._makeOne()
config = self._makeConfig()
diff --git a/pyramid/tests/test_response.py b/pyramid/tests/test_response.py
index a16eb8d33..84ec57757 100644
--- a/pyramid/tests/test_response.py
+++ b/pyramid/tests/test_response.py
@@ -1,4 +1,5 @@
import io
+import mimetypes
import os
import unittest
from pyramid import testing
@@ -51,15 +52,11 @@ class TestFileResponse(unittest.TestCase):
r.app_iter.close()
def test_without_content_type(self):
- for suffix, content_type in (
- ('txt', 'text/plain; charset=UTF-8'),
- ('xml', 'application/xml; charset=UTF-8'),
- ('pdf', 'application/pdf')
- ):
+ for suffix in ('txt', 'xml', 'pdf'):
path = self._getPath(suffix)
r = self._makeOne(path)
- self.assertEqual(r.content_type, content_type.split(';')[0])
- self.assertEqual(r.headers['content-type'], content_type)
+ self.assertEqual(r.headers['content-type'].split(';')[0],
+ mimetypes.guess_type(path, strict=False)[0])
r.app_iter.close()
def test_python_277_bug_15207(self):
diff --git a/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py
index 25a3cd2e3..32202af4b 100644
--- a/pyramid/tests/test_scripts/test_proutes.py
+++ b/pyramid/tests/test_scripts/test_proutes.py
@@ -123,8 +123,53 @@ class TestPRoutesCommand(unittest.TestCase):
self.assertEqual(result, 0)
self.assertEqual(len(L), 3)
compare_to = L[-1].split()[:3]
- self.assertEqual(compare_to, ['a', '/a', '<function'])
-
+ self.assertEqual(
+ compare_to,
+ ['a', '/a', 'pyramid.tests.test_scripts.test_proutes.view']
+ )
+
+ def test_one_route_with_long_name_one_view_registered(self):
+ from zope.interface import Interface
+ from pyramid.registry import Registry
+ from pyramid.interfaces import IRouteRequest
+ from pyramid.interfaces import IViewClassifier
+ from pyramid.interfaces import IView
+ registry = Registry()
+ def view():pass
+
+ class IMyRoute(Interface):
+ pass
+
+ registry.registerAdapter(
+ view,
+ (IViewClassifier, IMyRoute, Interface),
+ IView, ''
+ )
+
+ registry.registerUtility(IMyRoute, IRouteRequest,
+ name='very_long_name_123')
+
+ command = self._makeOne()
+ route = dummy.DummyRoute(
+ 'very_long_name_123',
+ '/and_very_long_pattern_as_well'
+ )
+ mapper = dummy.DummyMapper(route)
+ command._get_mapper = lambda *arg: mapper
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()[:3]
+ self.assertEqual(
+ compare_to,
+ ['very_long_name_123',
+ '/and_very_long_pattern_as_well',
+ 'pyramid.tests.test_scripts.test_proutes.view']
+ )
+
def test_single_route_one_view_registered_with_factory(self):
from zope.interface import Interface
from pyramid.registry import Registry
@@ -154,6 +199,47 @@ class TestPRoutesCommand(unittest.TestCase):
self.assertEqual(len(L), 3)
self.assertEqual(L[-1].split()[:3], ['a', '/a', '<unknown>'])
+ def test_single_route_multiview_registered(self):
+ from zope.interface import Interface
+ from pyramid.registry import Registry
+ from pyramid.interfaces import IRouteRequest
+ from pyramid.interfaces import IViewClassifier
+ from pyramid.interfaces import IMultiView
+
+ registry = Registry()
+
+ def view(): pass
+
+ class IMyRoute(Interface):
+ pass
+
+ multiview1 = dummy.DummyMultiView(
+ view, context='context',
+ view_name='a1'
+ )
+
+ registry.registerAdapter(
+ multiview1,
+ (IViewClassifier, IMyRoute, Interface),
+ IMultiView, ''
+ )
+ registry.registerUtility(IMyRoute, IRouteRequest, name='a')
+ command = self._makeOne()
+ route = dummy.DummyRoute('a', '/a')
+ mapper = dummy.DummyMapper(route)
+ command._get_mapper = lambda *arg: mapper
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()[:3]
+ self.assertEqual(
+ compare_to,
+ ['a', '/a', 'pyramid.tests.test_scripts.test_proutes.view']
+ )
+
def test__get_mapper(self):
from pyramid.registry import Registry
from pyramid.urldispatch import RoutesMapper
@@ -161,7 +247,7 @@ class TestPRoutesCommand(unittest.TestCase):
registry = Registry()
result = command._get_mapper(registry)
self.assertEqual(result.__class__, RoutesMapper)
-
+
class Test_main(unittest.TestCase):
def _callFUT(self, argv):
from pyramid.scripts.proutes import main
@@ -170,4 +256,3 @@ class Test_main(unittest.TestCase):
def test_it(self):
result = self._callFUT(['proutes'])
self.assertEqual(result, 2)
-
diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py
index 2ca4c4a66..a18fa8d16 100644
--- a/pyramid/tests/test_util.py
+++ b/pyramid/tests/test_util.py
@@ -217,6 +217,49 @@ class Test_WeakOrderedSet(unittest.TestCase):
self.assertEqual(list(wos), [])
self.assertEqual(wos.last, None)
+class Test_strings_differ(unittest.TestCase):
+ def _callFUT(self, *args, **kw):
+ from pyramid.util import strings_differ
+ return strings_differ(*args, **kw)
+
+ def test_it(self):
+ self.assertFalse(self._callFUT(b'foo', b'foo'))
+ self.assertTrue(self._callFUT(b'123', b'345'))
+ self.assertTrue(self._callFUT(b'1234', b'123'))
+ self.assertTrue(self._callFUT(b'123', b'1234'))
+
+ def test_it_with_internal_comparator(self):
+ result = self._callFUT(b'foo', b'foo', compare_digest=None)
+ self.assertFalse(result)
+
+ result = self._callFUT(b'123', b'abc', compare_digest=None)
+ self.assertTrue(result)
+
+ def test_it_with_external_comparator(self):
+ class DummyComparator(object):
+ called = False
+ def __init__(self, ret_val):
+ self.ret_val = ret_val
+
+ def __call__(self, a, b):
+ self.called = True
+ return self.ret_val
+
+ dummy_compare = DummyComparator(True)
+ result = self._callFUT(b'foo', b'foo', compare_digest=dummy_compare)
+ self.assertTrue(dummy_compare.called)
+ self.assertFalse(result)
+
+ dummy_compare = DummyComparator(False)
+ result = self._callFUT(b'123', b'345', compare_digest=dummy_compare)
+ self.assertTrue(dummy_compare.called)
+ self.assertTrue(result)
+
+ dummy_compare = DummyComparator(False)
+ result = self._callFUT(b'abc', b'abc', compare_digest=dummy_compare)
+ self.assertTrue(dummy_compare.called)
+ self.assertTrue(result)
+
class Test_object_description(unittest.TestCase):
def _callFUT(self, object):
from pyramid.util import object_description
diff --git a/pyramid/util.py b/pyramid/util.py
index 6b92f17fc..6de53d559 100644
--- a/pyramid/util.py
+++ b/pyramid/util.py
@@ -1,4 +1,9 @@
import functools
+try:
+ # py2.7.7+ and py3.3+ have native comparison support
+ from hmac import compare_digest
+except ImportError: # pragma: nocover
+ compare_digest = None
import inspect
import traceback
import weakref
@@ -227,7 +232,7 @@ class WeakOrderedSet(object):
oid = self._order[-1]
return self._items[oid]()
-def strings_differ(string1, string2):
+def strings_differ(string1, string2, compare_digest=compare_digest):
"""Check whether two strings differ while avoiding timing attacks.
This function returns True if the given strings differ and False
@@ -237,14 +242,25 @@ def strings_differ(string1, string2):
http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf
- """
- if len(string1) != len(string2):
- return True
-
- invalid_bits = 0
- for a, b in zip(string1, string2):
- invalid_bits += a != b
+ .. versionchanged:: 1.6
+ Support :func:`hmac.compare_digest` if it is available (Python 2.7.7+
+ and Python 3.3+).
+ """
+ len_eq = len(string1) == len(string2)
+ if len_eq:
+ invalid_bits = 0
+ left = string1
+ else:
+ invalid_bits = 1
+ left = string2
+ right = string2
+
+ if compare_digest is not None:
+ invalid_bits += not compare_digest(left, right)
+ else:
+ for a, b in zip(left, right):
+ invalid_bits += a != b
return invalid_bits != 0
def object_description(object):
diff --git a/tox.ini b/tox.ini
index 2bf213ca4..9a9c5a983 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,7 +12,7 @@ basepython =
python2.6
commands =
python setup.py dev
- python setup.py nosetests --with-xunit --with-xcoverage
+ python setup.py nosetests --with-xunit --with-xcoverage --cover-min-percentage=100
deps =
nosexcover