diff options
| author | Michael Merickel <michael@merickel.org> | 2014-11-17 02:18:15 -0600 |
|---|---|---|
| committer | Michael Merickel <michael@merickel.org> | 2014-11-17 02:18:15 -0600 |
| commit | 0c5e5ac9bbb9d0e1bc4aa8f3497f2c7f54ca96d3 (patch) | |
| tree | af1f326e83b8e799abe17bad6f1259185b52f813 | |
| parent | 823ac447329e46e5826e8e3228d9f847f9790ee8 (diff) | |
| parent | c0f1fc8d31df45371d5ae6689d3e0a39c058c3ac (diff) | |
| download | pyramid-0c5e5ac9bbb9d0e1bc4aa8f3497f2c7f54ca96d3.tar.gz pyramid-0c5e5ac9bbb9d0e1bc4aa8f3497f2c7f54ca96d3.tar.bz2 pyramid-0c5e5ac9bbb9d0e1bc4aa8f3497f2c7f54ca96d3.zip | |
Merge branch 'master' into feature.pshell-pythonstartup
| -rw-r--r-- | .travis.yml | 22 | ||||
| -rw-r--r-- | CHANGES.txt | 24 | ||||
| -rw-r--r-- | CONTRIBUTORS.txt | 2 | ||||
| -rw-r--r-- | docs/narr/i18n.rst | 2 | ||||
| -rw-r--r-- | pyramid/config/views.py | 21 | ||||
| -rw-r--r-- | pyramid/scripts/proutes.py | 68 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_views.py | 37 | ||||
| -rw-r--r-- | pyramid/tests/test_response.py | 11 | ||||
| -rw-r--r-- | pyramid/tests/test_scripts/test_proutes.py | 93 | ||||
| -rw-r--r-- | pyramid/tests/test_util.py | 43 | ||||
| -rw-r--r-- | pyramid/util.py | 32 | ||||
| -rw-r--r-- | tox.ini | 2 |
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): @@ -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 |
