From 407b335ed9954c042377fd2e060c36edcd07cf60 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 27 Feb 2014 23:45:24 -0500 Subject: add support for using an absolute path to override an asset fixes #1229 --- CHANGES.txt | 4 + docs/narr/assets.rst | 3 + pyramid/config/assets.py | 216 +++++++--- .../pkgs/asset/subpackage/templates/bar.pt | 0 pyramid/tests/test_config/test_assets.py | 439 +++++++++++++++------ 5 files changed, 496 insertions(+), 166 deletions(-) create mode 100644 pyramid/tests/test_config/pkgs/asset/subpackage/templates/bar.pt diff --git a/CHANGES.txt b/CHANGES.txt index 434eab898..2350bb3de 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +- Assets can now be overidden by an absolute path on the filesystem when using + the ``config.override_asset`` API. + See https://github.com/Pylons/pyramid/issues/1229 + Unreleased ========== diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index b0a8d18b0..fec55ce7c 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -526,3 +526,6 @@ files. Any software which uses the :func:`pkg_resources.get_resource_string` APIs will obtain an overridden file when an override is used. +As of Pyramid 1.6, it is also possible to override an asset by supplying an +absolute path to a file or directory. This may be useful if the assets are +not distributed as part of a Python package. diff --git a/pyramid/config/assets.py b/pyramid/config/assets.py index 0616e6cda..9da092f08 100644 --- a/pyramid/config/assets.py +++ b/pyramid/config/assets.py @@ -1,3 +1,4 @@ +import os import pkg_resources import sys @@ -79,7 +80,8 @@ class OverrideProvider(pkg_resources.DefaultProvider): return result return pkg_resources.DefaultProvider.resource_listdir( self, resource_name) - + + @implementer(IPackageOverrides) class PackageOverrides(object): # pkg_resources arg in kw args below for testing @@ -97,57 +99,61 @@ class PackageOverrides(object): # optional)... # A __loader__ attribute is basically metadata, and setuptools # uses it as such. - package.__loader__ = self + package.__loader__ = self # we call register_loader_type for every instantiation of this # class; that's OK, it's idempotent to do it more than once. pkg_resources.register_loader_type(self.__class__, OverrideProvider) self.overrides = [] self.overridden_package_name = package.__name__ - def insert(self, path, package, prefix): + def insert(self, path, source): if not path or path.endswith('/'): - override = DirectoryOverride(path, package, prefix) + override = DirectoryOverride(path, source) else: - override = FileOverride(path, package, prefix) + override = FileOverride(path, source) self.overrides.insert(0, override) return override - def search_path(self, resource_name): + def filtered_sources(self, resource_name): for override in self.overrides: o = override(resource_name) if o is not None: - package, name = o - yield package, name + yield o def get_filename(self, resource_name): - for package, rname in self.search_path(resource_name): - if pkg_resources.resource_exists(package, rname): - return pkg_resources.resource_filename(package, rname) + for source, path in self.filtered_sources(resource_name): + result = source.get_filename(path) + if result is not None: + return result def get_stream(self, resource_name): - for package, rname in self.search_path(resource_name): - if pkg_resources.resource_exists(package, rname): - return pkg_resources.resource_stream(package, rname) + for source, path in self.filtered_sources(resource_name): + result = source.get_stream(path) + if result is not None: + return result def get_string(self, resource_name): - for package, rname in self.search_path(resource_name): - if pkg_resources.resource_exists(package, rname): - return pkg_resources.resource_string(package, rname) + for source, path in self.filtered_sources(resource_name): + result = source.get_string(path) + if result is not None: + return result def has_resource(self, resource_name): - for package, rname in self.search_path(resource_name): - if pkg_resources.resource_exists(package, rname): + for source, path in self.filtered_sources(resource_name): + if source.exists(path): return True def isdir(self, resource_name): - for package, rname in self.search_path(resource_name): - if pkg_resources.resource_exists(package, rname): - return pkg_resources.resource_isdir(package, rname) + for source, path in self.filtered_sources(resource_name): + result = source.isdir(path) + if result is not None: + return result def listdir(self, resource_name): - for package, rname in self.search_path(resource_name): - if pkg_resources.resource_exists(package, rname): - return pkg_resources.resource_listdir(package, rname) + for source, path in self.filtered_sources(resource_name): + result = source.listdir(path) + if result is not None: + return result @property def real_loader(self): @@ -174,72 +180,180 @@ class PackageOverrides(object): """ See IPEP302Loader. """ return self.real_loader.get_source(fullname) - + class DirectoryOverride: - def __init__(self, path, package, prefix): + def __init__(self, path, source): self.path = path - self.package = package - self.prefix = prefix self.pathlen = len(self.path) + self.source = source def __call__(self, resource_name): if resource_name.startswith(self.path): - name = '%s%s' % (self.prefix, resource_name[self.pathlen:]) - return self.package, name + new_path = resource_name[self.pathlen:] + return self.source, new_path class FileOverride: - def __init__(self, path, package, prefix): + def __init__(self, path, source): self.path = path - self.package = package - self.prefix = prefix + self.source = source def __call__(self, resource_name): if resource_name == self.path: - return self.package, self.prefix + return self.source, '' + + +class PackageAssetSource(object): + """ + An asset source relative to a package. + + If this asset source is a file, then we expect the ``prefix`` to point + to the new name of the file, and the incoming ``resource_name`` will be + the empty string, as returned by the ``FileOverride``. + + """ + def __init__(self, package, prefix): + self.package = package + self.prefix = prefix + + def get_path(self, resource_name): + return '%s%s' % (self.prefix, resource_name) + + def get_filename(self, resource_name): + path = self.get_path(resource_name) + if pkg_resources.resource_exists(self.package, path): + return pkg_resources.resource_filename(self.package, path) + + def get_stream(self, resource_name): + path = self.get_path(resource_name) + if pkg_resources.resource_exists(self.package, path): + return pkg_resources.resource_stream(self.package, path) + + def get_string(self, resource_name): + path = self.get_path(resource_name) + if pkg_resources.resource_exists(self.package, path): + return pkg_resources.resource_string(self.package, path) + + def exists(self, resource_name): + path = self.get_path(resource_name) + if pkg_resources.resource_exists(self.package, path): + return True + + def isdir(self, resource_name): + path = self.get_path(resource_name) + if pkg_resources.resource_exists(self.package, path): + return pkg_resources.resource_isdir(self.package, path) + + def listdir(self, resource_name): + path = self.get_path(resource_name) + if pkg_resources.resource_exists(self.package, path): + return pkg_resources.resource_listdir(self.package, path) + + +class FSAssetSource(object): + """ + An asset source relative to a path in the filesystem. + + """ + def __init__(self, prefix): + self.prefix = prefix + + def get_filename(self, resource_name): + if resource_name: + path = os.path.join(self.prefix, resource_name) + else: + path = self.prefix + + if os.path.exists(path): + return path + + def get_stream(self, resource_name): + path = self.get_filename(resource_name) + if path is not None: + return open(path, 'rb') + + def get_string(self, resource_name): + stream = self.get_stream(resource_name) + if stream is not None: + with stream: + return stream.read() + + def exists(self, resource_name): + path = self.get_filename(resource_name) + if path is not None: + return True + + def isdir(self, resource_name): + path = self.get_filename(resource_name) + if path is not None: + return os.path.isdir(path) + + def listdir(self, resource_name): + path = self.get_filename(resource_name) + if path is not None: + return os.listdir(path) class AssetsConfiguratorMixin(object): - def _override(self, package, path, override_package, override_prefix, + def _override(self, package, path, override_source, PackageOverrides=PackageOverrides): pkg_name = package.__name__ - override_pkg_name = override_package.__name__ override = self.registry.queryUtility(IPackageOverrides, name=pkg_name) if override is None: override = PackageOverrides(package) self.registry.registerUtility(override, IPackageOverrides, name=pkg_name) - override.insert(path, override_pkg_name, override_prefix) + override.insert(path, override_source) @action_method def override_asset(self, to_override, override_with, _override=None): """ Add a :app:`Pyramid` asset override to the current configuration state. - ``to_override`` is a :term:`asset specification` to the + ``to_override`` is an :term:`asset specification` to the asset being overridden. - ``override_with`` is a :term:`asset specification` to the - asset that is performing the override. + ``override_with`` is an :term:`asset specification` to the + asset that is performing the override. This may also be an absolute + path. See :ref:`assets_chapter` for more information about asset overrides.""" if to_override == override_with: - raise ConfigurationError('You cannot override an asset with itself') + raise ConfigurationError( + 'You cannot override an asset with itself') package = to_override path = '' if ':' in to_override: package, path = to_override.split(':', 1) - override_package = override_with - override_prefix = '' - if ':' in override_with: - override_package, override_prefix = override_with.split(':', 1) - # *_isdir = override is package or directory - overridden_isdir = path=='' or path.endswith('/') - override_isdir = override_prefix=='' or override_prefix.endswith('/') + overridden_isdir = path == '' or path.endswith('/') + + if os.path.isabs(override_with): + override_source = FSAssetSource(override_with) + if not os.path.exists(override_with): + raise ConfigurationError( + 'Cannot override asset with an absolute path that does ' + 'not exist') + override_isdir = os.path.isdir(override_with) + override_package = None + override_prefix = override_with + else: + override_package = override_with + override_prefix = '' + if ':' in override_with: + override_package, override_prefix = override_with.split(':', 1) + + __import__(override_package) + to_package = sys.modules[override_package] + override_source = PackageAssetSource(to_package, override_prefix) + + override_isdir = ( + override_prefix == '' or + override_with.endswith('/') + ) if overridden_isdir and (not override_isdir): raise ConfigurationError( @@ -255,10 +369,8 @@ class AssetsConfiguratorMixin(object): def register(): __import__(package) - __import__(override_package) from_package = sys.modules[package] - to_package = sys.modules[override_package] - override(from_package, path, to_package, override_prefix) + override(from_package, path, override_source) intr = self.introspectable( 'asset overrides', diff --git a/pyramid/tests/test_config/pkgs/asset/subpackage/templates/bar.pt b/pyramid/tests/test_config/pkgs/asset/subpackage/templates/bar.pt new file mode 100644 index 000000000..e69de29bb diff --git a/pyramid/tests/test_config/test_assets.py b/pyramid/tests/test_config/test_assets.py index 345e7f8d6..b605a602d 100644 --- a/pyramid/tests/test_config/test_assets.py +++ b/pyramid/tests/test_config/test_assets.py @@ -1,6 +1,10 @@ +import os.path import unittest from pyramid.testing import cleanUp +# we use this folder +here = os.path.dirname(os.path.abspath(__file__)) + class TestAssetsConfiguratorMixin(unittest.TestCase): def _makeOne(self, *arg, **kw): from pyramid.config import Configurator @@ -10,27 +14,31 @@ class TestAssetsConfiguratorMixin(unittest.TestCase): def test_override_asset_samename(self): from pyramid.exceptions import ConfigurationError config = self._makeOne() - self.assertRaises(ConfigurationError, config.override_asset,'a', 'a') + self.assertRaises(ConfigurationError, config.override_asset, 'a', 'a') def test_override_asset_directory_with_file(self): from pyramid.exceptions import ConfigurationError config = self._makeOne() self.assertRaises(ConfigurationError, config.override_asset, - 'a:foo/', 'a:foo.pt') + 'a:foo/', + 'pyramid.tests.test_config.pkgs.asset:foo.pt') def test_override_asset_file_with_directory(self): from pyramid.exceptions import ConfigurationError config = self._makeOne() self.assertRaises(ConfigurationError, config.override_asset, - 'a:foo.pt', 'a:foo/') + 'a:foo.pt', + 'pyramid.tests.test_config.pkgs.asset:templates/') def test_override_asset_file_with_package(self): from pyramid.exceptions import ConfigurationError config = self._makeOne() self.assertRaises(ConfigurationError, config.override_asset, - 'a:foo.pt', 'a') + 'a:foo.pt', + 'pyramid.tests.test_config.pkgs.asset') def test_override_asset_file_with_file(self): + from pyramid.config.assets import PackageAssetSource config = self._makeOne(autocommit=True) override = DummyUnderOverride() config.override_asset( @@ -41,10 +49,13 @@ class TestAssetsConfiguratorMixin(unittest.TestCase): from pyramid.tests.test_config.pkgs.asset import subpackage self.assertEqual(override.package, asset) self.assertEqual(override.path, 'templates/foo.pt') - self.assertEqual(override.override_package, subpackage) - self.assertEqual(override.override_prefix, 'templates/bar.pt') + source = override.source + self.assertTrue(isinstance(source, PackageAssetSource)) + self.assertEqual(source.package, subpackage) + self.assertEqual(source.prefix, 'templates/bar.pt') def test_override_asset_package_with_package(self): + from pyramid.config.assets import PackageAssetSource config = self._makeOne(autocommit=True) override = DummyUnderOverride() config.override_asset( @@ -55,10 +66,13 @@ class TestAssetsConfiguratorMixin(unittest.TestCase): from pyramid.tests.test_config.pkgs.asset import subpackage self.assertEqual(override.package, asset) self.assertEqual(override.path, '') - self.assertEqual(override.override_package, subpackage) - self.assertEqual(override.override_prefix, '') + source = override.source + self.assertTrue(isinstance(source, PackageAssetSource)) + self.assertEqual(source.package, subpackage) + self.assertEqual(source.prefix, '') def test_override_asset_directory_with_directory(self): + from pyramid.config.assets import PackageAssetSource config = self._makeOne(autocommit=True) override = DummyUnderOverride() config.override_asset( @@ -69,10 +83,13 @@ class TestAssetsConfiguratorMixin(unittest.TestCase): from pyramid.tests.test_config.pkgs.asset import subpackage self.assertEqual(override.package, asset) self.assertEqual(override.path, 'templates/') - self.assertEqual(override.override_package, subpackage) - self.assertEqual(override.override_prefix, 'templates/') + source = override.source + self.assertTrue(isinstance(source, PackageAssetSource)) + self.assertEqual(source.package, subpackage) + self.assertEqual(source.prefix, 'templates/') def test_override_asset_directory_with_package(self): + from pyramid.config.assets import PackageAssetSource config = self._makeOne(autocommit=True) override = DummyUnderOverride() config.override_asset( @@ -83,10 +100,13 @@ class TestAssetsConfiguratorMixin(unittest.TestCase): from pyramid.tests.test_config.pkgs.asset import subpackage self.assertEqual(override.package, asset) self.assertEqual(override.path, 'templates/') - self.assertEqual(override.override_package, subpackage) - self.assertEqual(override.override_prefix, '') + source = override.source + self.assertTrue(isinstance(source, PackageAssetSource)) + self.assertEqual(source.package, subpackage) + self.assertEqual(source.prefix, '') def test_override_asset_package_with_directory(self): + from pyramid.config.assets import PackageAssetSource config = self._makeOne(autocommit=True) override = DummyUnderOverride() config.override_asset( @@ -97,32 +117,105 @@ class TestAssetsConfiguratorMixin(unittest.TestCase): from pyramid.tests.test_config.pkgs.asset import subpackage self.assertEqual(override.package, asset) self.assertEqual(override.path, '') - self.assertEqual(override.override_package, subpackage) - self.assertEqual(override.override_prefix, 'templates/') + source = override.source + self.assertTrue(isinstance(source, PackageAssetSource)) + self.assertEqual(source.package, subpackage) + self.assertEqual(source.prefix, 'templates/') + + def test_override_asset_directory_with_absfile(self): + from pyramid.exceptions import ConfigurationError + config = self._makeOne() + self.assertRaises(ConfigurationError, config.override_asset, + 'a:foo/', + os.path.join(here, 'pkgs', 'asset', 'foo.pt')) + + def test_override_asset_file_with_absdirectory(self): + from pyramid.exceptions import ConfigurationError + config = self._makeOne() + abspath = os.path.join(here, 'pkgs', 'asset', 'subpackage', 'templates') + self.assertRaises(ConfigurationError, config.override_asset, + 'a:foo.pt', + abspath) + + def test_override_asset_file_with_missing_abspath(self): + from pyramid.exceptions import ConfigurationError + config = self._makeOne() + self.assertRaises(ConfigurationError, config.override_asset, + 'a:foo.pt', + os.path.join(here, 'wont_exist')) + + def test_override_asset_file_with_absfile(self): + from pyramid.config.assets import FSAssetSource + config = self._makeOne(autocommit=True) + override = DummyUnderOverride() + abspath = os.path.join(here, 'pkgs', 'asset', 'subpackage', + 'templates', 'bar.pt') + config.override_asset( + 'pyramid.tests.test_config.pkgs.asset:templates/foo.pt', + abspath, + _override=override) + from pyramid.tests.test_config.pkgs import asset + self.assertEqual(override.package, asset) + self.assertEqual(override.path, 'templates/foo.pt') + source = override.source + self.assertTrue(isinstance(source, FSAssetSource)) + self.assertEqual(source.prefix, abspath) + + def test_override_asset_directory_with_absdirectory(self): + from pyramid.config.assets import FSAssetSource + config = self._makeOne(autocommit=True) + override = DummyUnderOverride() + abspath = os.path.join(here, 'pkgs', 'asset', 'subpackage', 'templates') + config.override_asset( + 'pyramid.tests.test_config.pkgs.asset:templates/', + abspath, + _override=override) + from pyramid.tests.test_config.pkgs import asset + self.assertEqual(override.package, asset) + self.assertEqual(override.path, 'templates/') + source = override.source + self.assertTrue(isinstance(source, FSAssetSource)) + self.assertEqual(source.prefix, abspath) + + def test_override_asset_package_with_absdirectory(self): + from pyramid.config.assets import FSAssetSource + config = self._makeOne(autocommit=True) + override = DummyUnderOverride() + abspath = os.path.join(here, 'pkgs', 'asset', 'subpackage', 'templates') + config.override_asset( + 'pyramid.tests.test_config.pkgs.asset', + abspath, + _override=override) + from pyramid.tests.test_config.pkgs import asset + self.assertEqual(override.package, asset) + self.assertEqual(override.path, '') + source = override.source + self.assertTrue(isinstance(source, FSAssetSource)) + self.assertEqual(source.prefix, abspath) def test__override_not_yet_registered(self): from pyramid.interfaces import IPackageOverrides package = DummyPackage('package') - opackage = DummyPackage('opackage') + source = DummyAssetSource() config = self._makeOne() - config._override(package, 'path', opackage, 'oprefix', + config._override(package, 'path', source, PackageOverrides=DummyPackageOverrides) overrides = config.registry.queryUtility(IPackageOverrides, name='package') - self.assertEqual(overrides.inserted, [('path', 'opackage', 'oprefix')]) + self.assertEqual(overrides.inserted, [('path', source)]) self.assertEqual(overrides.package, package) def test__override_already_registered(self): from pyramid.interfaces import IPackageOverrides package = DummyPackage('package') - opackage = DummyPackage('opackage') + source = DummyAssetSource() overrides = DummyPackageOverrides(package) config = self._makeOne() config.registry.registerUtility(overrides, IPackageOverrides, name='package') - config._override(package, 'path', opackage, 'oprefix', + config._override(package, 'path', source, PackageOverrides=DummyPackageOverrides) - self.assertEqual(overrides.inserted, [('path', 'opackage', 'oprefix')]) + self.assertEqual(overrides.inserted, [('path', source)]) self.assertEqual(overrides.package, package) @@ -148,30 +241,24 @@ class TestOverrideProvider(unittest.TestCase): reg.registerUtility(overrides, IPackageOverrides, name=name) def test_get_resource_filename_no_overrides(self): - import os resource_name = 'test_assets.py' import pyramid.tests.test_config provider = self._makeOne(pyramid.tests.test_config) - here = os.path.dirname(os.path.abspath(__file__)) expected = os.path.join(here, resource_name) result = provider.get_resource_filename(None, resource_name) self.assertEqual(result, expected) def test_get_resource_stream_no_overrides(self): - import os resource_name = 'test_assets.py' import pyramid.tests.test_config provider = self._makeOne(pyramid.tests.test_config) - here = os.path.dirname(os.path.abspath(__file__)) with provider.get_resource_stream(None, resource_name) as result: _assertBody(result.read(), os.path.join(here, resource_name)) def test_get_resource_string_no_overrides(self): - import os resource_name = 'test_assets.py' import pyramid.tests.test_config provider = self._makeOne(pyramid.tests.test_config) - here = os.path.dirname(os.path.abspath(__file__)) result = provider.get_resource_string(None, resource_name) _assertBody(result, os.path.join(here, resource_name)) @@ -202,11 +289,9 @@ class TestOverrideProvider(unittest.TestCase): def test_get_resource_filename_override_returns_None(self): overrides = DummyOverrides(None) self._registerOverrides(overrides) - import os resource_name = 'test_assets.py' import pyramid.tests.test_config provider = self._makeOne(pyramid.tests.test_config) - here = os.path.dirname(os.path.abspath(__file__)) expected = os.path.join(here, resource_name) result = provider.get_resource_filename(None, resource_name) self.assertEqual(result, expected) @@ -214,22 +299,18 @@ class TestOverrideProvider(unittest.TestCase): def test_get_resource_stream_override_returns_None(self): overrides = DummyOverrides(None) self._registerOverrides(overrides) - import os resource_name = 'test_assets.py' import pyramid.tests.test_config provider = self._makeOne(pyramid.tests.test_config) - here = os.path.dirname(os.path.abspath(__file__)) with provider.get_resource_stream(None, resource_name) as result: _assertBody(result.read(), os.path.join(here, resource_name)) def test_get_resource_string_override_returns_None(self): overrides = DummyOverrides(None) self._registerOverrides(overrides) - import os resource_name = 'test_assets.py' import pyramid.tests.test_config provider = self._makeOne(pyramid.tests.test_config) - here = os.path.dirname(os.path.abspath(__file__)) result = provider.get_resource_string(None, resource_name) _assertBody(result, os.path.join(here, resource_name)) @@ -378,8 +459,8 @@ class TestPackageOverrides(unittest.TestCase): from pyramid.config.assets import DirectoryOverride package = DummyPackage('package') po = self._makeOne(package) - po.overrides= [None] - po.insert('foo/', 'package', 'bar/') + po.overrides = [None] + po.insert('foo/', DummyAssetSource()) self.assertEqual(len(po.overrides), 2) override = po.overrides[0] self.assertEqual(override.__class__, DirectoryOverride) @@ -388,8 +469,8 @@ class TestPackageOverrides(unittest.TestCase): from pyramid.config.assets import FileOverride package = DummyPackage('package') po = self._makeOne(package) - po.overrides= [None] - po.insert('foo.pt', 'package', 'bar.pt') + po.overrides = [None] + po.insert('foo.pt', DummyAssetSource()) self.assertEqual(len(po.overrides), 2) override = po.overrides[0] self.assertEqual(override.__class__, FileOverride) @@ -399,132 +480,137 @@ class TestPackageOverrides(unittest.TestCase): from pyramid.config.assets import DirectoryOverride package = DummyPackage('package') po = self._makeOne(package) - po.overrides= [None] - po.insert('', 'package', 'bar/') + po.overrides = [None] + source = DummyAssetSource() + po.insert('', source) self.assertEqual(len(po.overrides), 2) override = po.overrides[0] self.assertEqual(override.__class__, DirectoryOverride) - def test_search_path(self): - overrides = [ DummyOverride(None), DummyOverride(('package', 'name'))] + def test_filtered_sources(self): + overrides = [ DummyOverride(None), DummyOverride('foo')] package = DummyPackage('package') po = self._makeOne(package) - po.overrides= overrides - self.assertEqual(list(po.search_path('whatever')), - [('package', 'name')]) + po.overrides = overrides + self.assertEqual(list(po.filtered_sources('whatever')), ['foo']) def test_get_filename(self): - import os - overrides = [ DummyOverride(None), DummyOverride( - ('pyramid.tests.test_config', 'test_assets.py'))] + source = DummyAssetSource(filename='foo.pt') + overrides = [ DummyOverride(None), DummyOverride((source, ''))] package = DummyPackage('package') po = self._makeOne(package) - po.overrides= overrides - here = os.path.dirname(os.path.abspath(__file__)) - expected = os.path.join(here, 'test_assets.py') - self.assertEqual(po.get_filename('whatever'), expected) + po.overrides = overrides + result = po.get_filename('whatever') + self.assertEqual(result, 'foo.pt') + self.assertEqual(source.resource_name, '') def test_get_filename_file_doesnt_exist(self): - overrides = [ DummyOverride(None), DummyOverride( - ('pyramid.tests.test_config', 'wont_exist'))] + source = DummyAssetSource(filename=None) + overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))] package = DummyPackage('package') po = self._makeOne(package) - po.overrides= overrides + po.overrides = overrides self.assertEqual(po.get_filename('whatever'), None) - + self.assertEqual(source.resource_name, 'wont_exist') + def test_get_stream(self): - import os - overrides = [ DummyOverride(None), DummyOverride( - ('pyramid.tests.test_config', 'test_assets.py'))] + source = DummyAssetSource(stream='a stream?') + overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))] package = DummyPackage('package') po = self._makeOne(package) - po.overrides= overrides - here = os.path.dirname(os.path.abspath(__file__)) - with po.get_stream('whatever') as stream: - _assertBody(stream.read(), os.path.join(here, 'test_assets.py')) + po.overrides = overrides + self.assertEqual(po.get_stream('whatever'), 'a stream?') + self.assertEqual(source.resource_name, 'foo.pt') def test_get_stream_file_doesnt_exist(self): - overrides = [ DummyOverride(None), DummyOverride( - ('pyramid.tests.test_config', 'wont_exist'))] + source = DummyAssetSource(stream=None) + overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))] package = DummyPackage('package') po = self._makeOne(package) - po.overrides= overrides + po.overrides = overrides self.assertEqual(po.get_stream('whatever'), None) + self.assertEqual(source.resource_name, 'wont_exist') def test_get_string(self): - import os - overrides = [ DummyOverride(None), DummyOverride( - ('pyramid.tests.test_config', 'test_assets.py'))] + source = DummyAssetSource(string='a string') + overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))] package = DummyPackage('package') po = self._makeOne(package) - po.overrides= overrides - here = os.path.dirname(os.path.abspath(__file__)) - _assertBody(po.get_string('whatever'), - os.path.join(here, 'test_assets.py')) + po.overrides = overrides + self.assertEqual(po.get_string('whatever'), 'a string') + self.assertEqual(source.resource_name, 'foo.pt') def test_get_string_file_doesnt_exist(self): - overrides = [ DummyOverride(None), DummyOverride( - ('pyramid.tests.test_config', 'wont_exist'))] + source = DummyAssetSource(string=None) + overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))] package = DummyPackage('package') po = self._makeOne(package) - po.overrides= overrides + po.overrides = overrides self.assertEqual(po.get_string('whatever'), None) + self.assertEqual(source.resource_name, 'wont_exist') def test_has_resource(self): - overrides = [ DummyOverride(None), DummyOverride( - ('pyramid.tests.test_config', 'test_assets.py'))] + source = DummyAssetSource(exists=True) + overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))] package = DummyPackage('package') po = self._makeOne(package) - po.overrides= overrides + po.overrides = overrides self.assertEqual(po.has_resource('whatever'), True) + self.assertEqual(source.resource_name, 'foo.pt') def test_has_resource_file_doesnt_exist(self): - overrides = [ DummyOverride(None), DummyOverride( - ('pyramid.tests.test_config', 'wont_exist'))] + source = DummyAssetSource(exists=None) + overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))] package = DummyPackage('package') po = self._makeOne(package) - po.overrides= overrides + po.overrides = overrides self.assertEqual(po.has_resource('whatever'), None) + self.assertEqual(source.resource_name, 'wont_exist') def test_isdir_false(self): - overrides = [ DummyOverride( - ('pyramid.tests.test_config', 'test_assets.py'))] + source = DummyAssetSource(isdir=False) + overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))] package = DummyPackage('package') po = self._makeOne(package) - po.overrides= overrides + po.overrides = overrides self.assertEqual(po.isdir('whatever'), False) - + self.assertEqual(source.resource_name, 'foo.pt') + def test_isdir_true(self): - overrides = [ DummyOverride( - ('pyramid.tests.test_config', 'files'))] + source = DummyAssetSource(isdir=True) + overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))] package = DummyPackage('package') po = self._makeOne(package) - po.overrides= overrides + po.overrides = overrides self.assertEqual(po.isdir('whatever'), True) + self.assertEqual(source.resource_name, 'foo.pt') def test_isdir_doesnt_exist(self): - overrides = [ DummyOverride(None), DummyOverride( - ('pyramid.tests.test_config', 'wont_exist'))] + source = DummyAssetSource(isdir=None) + overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))] package = DummyPackage('package') po = self._makeOne(package) - po.overrides= overrides + po.overrides = overrides self.assertEqual(po.isdir('whatever'), None) + self.assertEqual(source.resource_name, 'wont_exist') def test_listdir(self): - overrides = [ DummyOverride( - ('pyramid.tests.test_config', 'files'))] + source = DummyAssetSource(listdir=True) + overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))] package = DummyPackage('package') po = self._makeOne(package) - po.overrides= overrides - self.assertTrue(po.listdir('whatever')) + po.overrides = overrides + self.assertEqual(po.listdir('whatever'), True) + self.assertEqual(source.resource_name, 'foo.pt') def test_listdir_doesnt_exist(self): - overrides = [ DummyOverride(None), DummyOverride( - ('pyramid.tests.test_config', 'wont_exist'))] + source = DummyAssetSource(listdir=None) + overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))] package = DummyPackage('package') po = self._makeOne(package) - po.overrides= overrides + po.overrides = overrides self.assertEqual(po.listdir('whatever'), None) + self.assertEqual(source.resource_name, 'wont_exist') # PEP 302 __loader__ extensions: use the "real" __loader__, if present. def test_get_data_pkg_has_no___loader__(self): @@ -570,27 +656,124 @@ class TestPackageOverrides(unittest.TestCase): def test_get_source_pkg_has___loader__(self): package = DummyPackage('package') - loader = package.__loader__ = DummyLoader() + loader = package.__loader__ = DummyLoader() po = self._makeOne(package) self.assertEqual(po.get_source('whatever'), 'def foo():\n pass') self.assertEqual(loader._got_source, 'whatever') +class AssetSourceIntegrationTests(object): + + def test_get_filename(self): + source = self._makeOne('') + self.assertEqual(source.get_filename('test_assets.py'), + os.path.join(here, 'test_assets.py')) + + def test_get_filename_with_prefix(self): + source = self._makeOne('test_assets.py') + self.assertEqual(source.get_filename(''), + os.path.join(here, 'test_assets.py')) + + def test_get_filename_file_doesnt_exist(self): + source = self._makeOne('') + self.assertEqual(source.get_filename('wont_exist'), None) + + def test_get_stream(self): + source = self._makeOne('') + with source.get_stream('test_assets.py') as stream: + _assertBody(stream.read(), os.path.join(here, 'test_assets.py')) + + def test_get_stream_with_prefix(self): + source = self._makeOne('test_assets.py') + with source.get_stream('') as stream: + _assertBody(stream.read(), os.path.join(here, 'test_assets.py')) + + def test_get_stream_file_doesnt_exist(self): + source = self._makeOne('') + self.assertEqual(source.get_stream('wont_exist'), None) + + def test_get_string(self): + source = self._makeOne('') + _assertBody(source.get_string('test_assets.py'), + os.path.join(here, 'test_assets.py')) + + def test_get_string_with_prefix(self): + source = self._makeOne('test_assets.py') + _assertBody(source.get_string(''), + os.path.join(here, 'test_assets.py')) + + def test_get_string_file_doesnt_exist(self): + source = self._makeOne('') + self.assertEqual(source.get_string('wont_exist'), None) + + def test_exists(self): + source = self._makeOne('') + self.assertEqual(source.exists('test_assets.py'), True) + + def test_exists_with_prefix(self): + source = self._makeOne('test_assets.py') + self.assertEqual(source.exists(''), True) + + def test_exists_file_doesnt_exist(self): + source = self._makeOne('') + self.assertEqual(source.exists('wont_exist'), None) + + def test_isdir_false(self): + source = self._makeOne('') + self.assertEqual(source.isdir('test_assets.py'), False) + + def test_isdir_true(self): + source = self._makeOne('') + self.assertEqual(source.isdir('files'), True) + + def test_isdir_doesnt_exist(self): + source = self._makeOne('') + self.assertEqual(source.isdir('wont_exist'), None) + + def test_listdir(self): + source = self._makeOne('') + self.assertTrue(source.listdir('files')) + + def test_listdir_doesnt_exist(self): + source = self._makeOne('') + self.assertEqual(source.listdir('wont_exist'), None) + +class TestPackageAssetSource(AssetSourceIntegrationTests, unittest.TestCase): + + def _getTargetClass(self): + from pyramid.config.assets import PackageAssetSource + return PackageAssetSource + + def _makeOne(self, prefix, package='pyramid.tests.test_config'): + klass = self._getTargetClass() + return klass(package, prefix) + +class TestFSAssetSource(AssetSourceIntegrationTests, unittest.TestCase): + def _getTargetClass(self): + from pyramid.config.assets import FSAssetSource + return FSAssetSource + + def _makeOne(self, prefix, base_prefix=here): + klass = self._getTargetClass() + return klass(os.path.join(base_prefix, prefix)) + class TestDirectoryOverride(unittest.TestCase): def _getTargetClass(self): from pyramid.config.assets import DirectoryOverride return DirectoryOverride - def _makeOne(self, path, package, prefix): + def _makeOne(self, path, source): klass = self._getTargetClass() - return klass(path, package, prefix) + return klass(path, source) def test_it_match(self): - o = self._makeOne('foo/', 'package', 'bar/') + source = DummyAssetSource() + o = self._makeOne('foo/', source) result = o('foo/something.pt') - self.assertEqual(result, ('package', 'bar/something.pt')) + self.assertEqual(result, (source, 'something.pt')) def test_it_no_match(self): - o = self._makeOne('foo/', 'package', 'bar/') + source = DummyAssetSource() + o = self._makeOne('foo/', source) result = o('baz/notfound.pt') self.assertEqual(result, None) @@ -599,17 +782,19 @@ class TestFileOverride(unittest.TestCase): from pyramid.config.assets import FileOverride return FileOverride - def _makeOne(self, path, package, prefix): + def _makeOne(self, path, source): klass = self._getTargetClass() - return klass(path, package, prefix) + return klass(path, source) def test_it_match(self): - o = self._makeOne('foo.pt', 'package', 'bar.pt') + source = DummyAssetSource() + o = self._makeOne('foo.pt', source) result = o('foo.pt') - self.assertEqual(result, ('package', 'bar.pt')) + self.assertEqual(result, (source, '')) def test_it_no_match(self): - o = self._makeOne('foo.pt', 'package', 'bar.pt') + source = DummyAssetSource() + o = self._makeOne('foo.pt', source) result = o('notfound.pt') self.assertEqual(result, None) @@ -634,8 +819,8 @@ class DummyPackageOverrides: self.package = package self.inserted = [] - def insert(self, path, package, prefix): - self.inserted.append((path, package, prefix)) + def insert(self, path, source): + self.inserted.append((path, source)) class DummyPkgResources: def __init__(self): @@ -647,6 +832,34 @@ class DummyPkgResources: class DummyPackage: def __init__(self, name): self.__name__ = name + +class DummyAssetSource: + def __init__(self, **kw): + self.kw = kw + + def get_filename(self, resource_name): + self.resource_name = resource_name + return self.kw['filename'] + + def get_stream(self, resource_name): + self.resource_name = resource_name + return self.kw['stream'] + + def get_string(self, resource_name): + self.resource_name = resource_name + return self.kw['string'] + + def exists(self, resource_name): + self.resource_name = resource_name + return self.kw['exists'] + + def isdir(self, resource_name): + self.resource_name = resource_name + return self.kw['isdir'] + + def listdir(self, resource_name): + self.resource_name = resource_name + return self.kw['listdir'] class DummyLoader: _got_data = _is_package = None @@ -664,12 +877,10 @@ class DummyLoader: return 'def foo():\n pass' class DummyUnderOverride: - def __call__(self, package, path, override_package, override_prefix, - _info=''): + def __call__(self, package, path, source, _info=''): self.package = package self.path = path - self.override_package = override_package - self.override_prefix = override_prefix + self.source = source def read_(src): with open(src, 'rb') as f: -- cgit v1.2.3 From 6d37c8632b713af56c366c3ebeca55a89b30b57a Mon Sep 17 00:00:00 2001 From: John Kraal Date: Fri, 25 Apr 2014 10:00:31 +0200 Subject: added line numbers to generic log formatter --- pyramid/scaffolds/alchemy/production.ini_tmpl | 2 +- pyramid/scaffolds/starter/production.ini_tmpl | 2 +- pyramid/scaffolds/zodb/production.ini_tmpl | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyramid/scaffolds/alchemy/production.ini_tmpl b/pyramid/scaffolds/alchemy/production.ini_tmpl index b316ec9ca..022bc0b7b 100644 --- a/pyramid/scaffolds/alchemy/production.ini_tmpl +++ b/pyramid/scaffolds/alchemy/production.ini_tmpl @@ -59,4 +59,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/pyramid/scaffolds/starter/production.ini_tmpl b/pyramid/scaffolds/starter/production.ini_tmpl index 6a123abf5..b2681c71d 100644 --- a/pyramid/scaffolds/starter/production.ini_tmpl +++ b/pyramid/scaffolds/starter/production.ini_tmpl @@ -51,4 +51,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/pyramid/scaffolds/zodb/production.ini_tmpl b/pyramid/scaffolds/zodb/production.ini_tmpl index c231e159d..522ff7651 100644 --- a/pyramid/scaffolds/zodb/production.ini_tmpl +++ b/pyramid/scaffolds/zodb/production.ini_tmpl @@ -57,4 +57,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s -- cgit v1.2.3 From 6c6bd98b59f97492458cc4154d439e581d4b420a Mon Sep 17 00:00:00 2001 From: Corey Farwell Date: Thu, 19 Jun 2014 22:01:47 -0700 Subject: Enable automated testing with Python 3.4 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 29e499e76..ce27b5ec3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ python: - pypy - 3.2 - 3.3 + - 3.4 install: python setup.py dev -- cgit v1.2.3 From c688c70fb2bf6731bbdbf68682eebb203b540a04 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 2 Jul 2014 14:36:27 -0400 Subject: dont need to use any settings, we include pyramid_jinja2 in main --- docs/quick_tutorial/jinja2.rst | 6 ------ docs/quick_tutorial/jinja2/tutorial/tests.py | 8 +------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/docs/quick_tutorial/jinja2.rst b/docs/quick_tutorial/jinja2.rst index 2f1e295dd..ad6da7a9e 100644 --- a/docs/quick_tutorial/jinja2.rst +++ b/docs/quick_tutorial/jinja2.rst @@ -45,12 +45,6 @@ Steps .. literalinclude:: jinja2/tutorial/home.jinja2 :language: html -#. Get the ``pyramid.includes`` into the functional test setup in - ``jinja2/tutorial/tests.py``: - - .. literalinclude:: jinja2/tutorial/tests.py - :linenos: - #. Now run the tests: .. code-block:: bash diff --git a/docs/quick_tutorial/jinja2/tutorial/tests.py b/docs/quick_tutorial/jinja2/tutorial/tests.py index 0b22946f3..4381235ec 100644 --- a/docs/quick_tutorial/jinja2/tutorial/tests.py +++ b/docs/quick_tutorial/jinja2/tutorial/tests.py @@ -30,13 +30,7 @@ class TutorialViewTests(unittest.TestCase): class TutorialFunctionalTests(unittest.TestCase): def setUp(self): from tutorial import main - - settings = { - 'pyramid.includes': [ - 'pyramid_jinja2' - ] - } - app = main({}, **settings) + app = main({}) from webtest import TestApp self.testapp = TestApp(app) -- cgit v1.2.3 From 7dd856ffed7df4bec637c29f4529cabc07e5e191 Mon Sep 17 00:00:00 2001 From: LiJunjie Date: Mon, 16 Jun 2014 14:36:30 +0800 Subject: Correct handler name for logger_wsgi --- docs/narr/logging.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/logging.rst b/docs/narr/logging.rst index 75428d513..71029bb33 100644 --- a/docs/narr/logging.rst +++ b/docs/narr/logging.rst @@ -377,7 +377,7 @@ FileHandler to the list of handlers (named ``accesslog``), and ensure that the [logger_wsgi] level = INFO - handlers = handler_accesslog + handlers = accesslog qualname = wsgi propagate = 0 -- cgit v1.2.3 From 3f87c228c920edbb85a85f3332a5340063e49b11 Mon Sep 17 00:00:00 2001 From: Kamal Gill Date: Mon, 2 Jun 2014 13:51:09 -0700 Subject: Fix path to pshell --- docs/narr/commandline.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index 3cabbd8f4..4f16617c4 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -146,7 +146,7 @@ name ``main`` as a section name: .. code-block:: text - $ $VENV/bin starter/development.ini#main + $ $VENV/bin/pshell starter/development.ini#main Python 2.6.5 (r265:79063, Apr 29 2010, 00:31:32) [GCC 4.4.3] on linux2 Type "help" for more information. -- cgit v1.2.3 From 36d5a438308dbd2a89d4da82eafbd5b368389348 Mon Sep 17 00:00:00 2001 From: Harz-FEAR Date: Tue, 8 Jul 2014 04:40:52 +0200 Subject: RFC 6585 HTTP Exceptions --- pyramid/httpexceptions.py | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index ebee39ada..7cf802139 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -52,6 +52,9 @@ Exception * 422 - HTTPUnprocessableEntity * 423 - HTTPLocked * 424 - HTTPFailedDependency + * 428 - HTTPPreconditionRequired + * 429 - HTTPTooManyRequests + * 431 - HTTPRequestHeaderFieldsTooLarge HTTPServerError * 500 - HTTPInternalServerError * 501 - HTTPNotImplemented @@ -60,6 +63,7 @@ Exception * 504 - HTTPGatewayTimeout * 505 - HTTPVersionNotSupported * 507 - HTTPInsufficientStorage + * 511 - HTTPNetworkAuthenticationRequired HTTP exceptions are also :term:`response` objects, thus they accept most of the same parameters that can be passed to a regular @@ -907,6 +911,62 @@ class HTTPFailedDependency(HTTPClientError): 'The method could not be performed because the requested ' 'action dependended on another action and that action failed') +class HTTPPreconditionRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the origin server requires the + request to be conditional. + + Its typical use is to avoid the "lost update" problem, where a client + GETs a resource's state, modifies it, and PUTs it back to the server, + when meanwhile a third party has modified the state on the server, + leading to a conflict. By requiring requests to be conditional, the + server can assure that clients are working with the correct copies. + + RFC 6585.3 + + code: 428, title: Precondition Required + """ + code = 428 + title = 'Precondition Required' + explanation = ( + 'The origin server requires the request to be conditional.') + +class HTTPTooManyRequests(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the user has sent too many + requests in a given amount of time ("rate limiting"). + + RFC 6585.4 + + code: 429, title: Too Many Requests + """ + code = 429 + title = 'Too Many Requests' + explanation = ( + 'The action could not be performed because there were too ' + 'many requests by the client.') + +class HTTPRequestHeaderFieldsTooLarge(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is unwilling to process + the request because its header fields are too large. The request MAY + be resubmitted after reducing the size of the request header fields. + + RFC 6585.5 + + code: 431, title: Request Header Fields Too Large + """ + code = 431 + title = 'Request Header Fields Too Large' + explanation = ( + 'The requests header fields were too large.') + ############################################################ ## 5xx Server Error ############################################################ -- cgit v1.2.3 From 5ef159eb1a86046da4c53ca9530fff0eb3b3432f Mon Sep 17 00:00:00 2001 From: Harz-FEAR Date: Tue, 8 Jul 2014 04:44:32 +0200 Subject: did not include code 511 because it is for use by intercepting proxies --- pyramid/httpexceptions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index 7cf802139..ea40a99af 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -63,7 +63,6 @@ Exception * 504 - HTTPGatewayTimeout * 505 - HTTPVersionNotSupported * 507 - HTTPInsufficientStorage - * 511 - HTTPNetworkAuthenticationRequired HTTP exceptions are also :term:`response` objects, thus they accept most of the same parameters that can be passed to a regular -- cgit v1.2.3 From eae99acbf9eed71967ff12961e495f27708d1f39 Mon Sep 17 00:00:00 2001 From: dobesv Date: Wed, 9 Jul 2014 15:15:08 -0700 Subject: Allow the last callback called to add a callback This fixes a bug in the finished and response callbacks where if the last/only callback adds another callback, the newly added callback won't be called afterwards. This is because when it tries to add the callback, it is added to a new list instance because the callbacks list is empty at that time; the check for whether the callbacks list was created didn't previously distinguish between an empty list and not a list. However, if it is not the last callback in the list, the callbacks list will not be empty and the new callback will be added to the same list and the newly added callback *will* be called. Because the code as written appears to be trying to support callbacks adding callbacks, this push request modifies the code so that a callback may add another callback whether it is the last one or not. An alternative approach would be to modify the code so that callbacks cannot add new callbacks, which also would be reasonable. But I think it's a bug that the behavior depends currently on whether you are in the last/only callback when you try to add another one. --- pyramid/request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/request.py b/pyramid/request.py index 6318049ee..7e8b8c07d 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -72,7 +72,7 @@ class CallbackMethodsMixin(object): """ callbacks = self.response_callbacks - if not callbacks: + if callbacks == (): callbacks = [] callbacks.append(callback) self.response_callbacks = callbacks @@ -132,7 +132,7 @@ class CallbackMethodsMixin(object): """ callbacks = self.finished_callbacks - if not callbacks: + if callbacks == (): callbacks = [] callbacks.append(callback) self.finished_callbacks = callbacks -- cgit v1.2.3 From cd299ae7a21a95c1023c2b7c38624234ead1d464 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 10 Jul 2014 14:08:47 -0700 Subject: Allow hyphens in project name Convert hyphens in project_name to underscores in pkg_name --- pyramid/scripts/pcreate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index 4c1f432fb..edf2c39f7 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -81,7 +81,8 @@ class PCreateCommand(object): args = self.args output_dir = os.path.abspath(os.path.normpath(args[0])) project_name = os.path.basename(os.path.split(output_dir)[1]) - pkg_name = _bad_chars_re.sub('', project_name.lower()) + pkg_name = _bad_chars_re.sub( + '', project_name.lower().replace('-', '_')) safe_name = pkg_resources.safe_name(project_name) egg_name = pkg_resources.to_filename(safe_name) -- cgit v1.2.3 From 7a479d270d654f61fca00f8a27b64fd2bf99d35d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 13 Jul 2014 23:39:38 -0400 Subject: remove lie --- docs/narr/templates.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/narr/templates.rst b/docs/narr/templates.rst index 460cda8ee..4c1364493 100644 --- a/docs/narr/templates.rst +++ b/docs/narr/templates.rst @@ -316,8 +316,7 @@ template renderer: we're using a Chameleon renderer, it means "relative to the directory in which the file which defines the view configuration lives". In this case, this is the directory containing the file that defines the ``my_view`` - function. View-configuration-relative asset specifications work only - in Chameleon, not in Mako templates. + function. Similar renderer configuration can be done imperatively. See :ref:`views_which_use_a_renderer`. -- cgit v1.2.3 From a8eb53fb79981e1b6fb93af3c80a6bdbae7f9d8f Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Mon, 14 Jul 2014 11:13:28 -0400 Subject: Narrative scifi. --- docs/narr/assets.rst | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index b0a8d18b0..a2976de22 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -286,6 +286,71 @@ the application is being run in development or in production (use a different suggestion for a pattern; any setting name other than ``media_location`` could be used. +.. _cache_busting: + +Cache Busting +------------- + +In order to maximize performance of a web application, you generally want to +limit the number of times a particular client requests the same static asset. +Ideally a client would cache a particular static asset "forever", requiring +it to be sent to the client a single time. The HTTP protocol allows you to +send headers with an HTTP response that can instruct a client to cache a +particular asset for an amount of time. As long as the client has a copy of +the asset in its cache and that cache hasn't expired, the client will use the +cached copy rather than request a new copy from the server. The drawback to +sending cache headers to the client for a static asset is that at some point +the static asset may change, and then you'll want the client to load a new copy +of the asset. Under normal circumstances you'd just need to wait for the +client's cached copy to expire before they get the new version of the static +resource. + +A commonly used workaround to this problem is a technique known as "cache +busting". Cache busting schemes generally involve generating a URL for a +static asset that changes when the static asset changes. This way headers can +be sent along with the static asset instructing the client to cache the asset +for a very long time. When a static asset is changed, the URL used to refer to +it in a web page also changes, so the client sees it as a new resource and +requests a copy, regardless of any caching policy set for the resource's old +URL. + +:app:`Pyramid` can be configured to produce cache busting URLs for static +assets by passing the optional argument, `cache_bust` to +:meth:`~pyramid.config.Configurator.add_static_view`: + +.. code-block:: python + :linenos: + + # config is an instance of pyramid.config.Configurator + config.add_static_view(name='static', path='mypackage:folder/static', + cache_bust='md5') + +Supplying the `cache_bust` argument instructs :app:`Pyramid` to add a query +string to URLs generated for this static view which includes the md5 checksum +of the static file being served: + +.. code-block:: python + :linenos: + + js_url = request.static_url('mypackage:folder/static/js/myapp.js') + # Returns: 'http://www.example.com/static/js/myapp.js?md5=c9658b3c0a314a1ca21e5988e662a09e` + +When the asset changes, so will its md5 checksum, and therefore so will its +URL. Supplying the `cache_bust` argument also causes the static view to set +headers instructing clients to cache the asset for ten years, unless the +`max_cache_age` argument is also passed, in which case that value is used. + +.. note:: + + `md5` is currently the only possible value for the `cache_bust` argument to + :meth:`~pyramid.config.Configurator.add_static_view`. + +.. note:: + + md5 checksums are cached in RAM so if you change a static resource without + restarting your application, you may still generate URLs with a stale md5 + checksum. + .. index:: single: static assets view -- cgit v1.2.3 From b648516a5dd61b3ce155586465f473338c230bf9 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Mon, 14 Jul 2014 11:25:33 -0400 Subject: API docs scifi. --- pyramid/config/views.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 7a6157ec8..d938a7632 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -302,7 +302,7 @@ class ViewDeriver(object): raise PredicateMismatch( 'predicate mismatch for view %s (%s)' % ( view_name, predicate.text())) - return view(context, request) + return view(context, request) def checker(context, request): return all((predicate(context, request) for predicate in preds)) @@ -894,8 +894,8 @@ class ViewsConfiguratorMixin(object): request_param - This value can be any string or any sequence of strings. A view - declaration with this argument ensures that the view will only be + This value can be any string or any sequence of strings. A view + declaration with this argument ensures that the view will only be called when the :term:`request` has a key in the ``request.params`` dictionary (an HTTP ``GET`` or ``POST`` variable) that has a name which matches the supplied value (if the value is a string) @@ -1001,7 +1001,7 @@ class ViewsConfiguratorMixin(object): Note that using this feature requires a :term:`session factory` to have been configured. - + .. versionadded:: 1.4a2 physical_path @@ -1039,7 +1039,7 @@ class ViewsConfiguratorMixin(object): This value should be a sequence of references to custom predicate callables. Use custom predicates when no set of predefined predicates do what you need. Custom predicates - can be combined with predefined predicates as necessary. + can be combined with predefined predicates as necessary. Each custom predicate callable should accept two arguments: ``context`` and ``request`` and should return either ``True`` or ``False`` after doing arbitrary evaluation of @@ -1074,7 +1074,7 @@ class ViewsConfiguratorMixin(object): DeprecationWarning, stacklevel=4 ) - + view = self.maybe_dotted(view) context = self.maybe_dotted(context) for_ = self.maybe_dotted(for_) @@ -1160,7 +1160,7 @@ class ViewsConfiguratorMixin(object): view_desc = self.object_description(view) tmpl_intr = None - + view_intr = self.introspectable('views', discriminator, view_desc, @@ -1569,7 +1569,7 @@ class ViewsConfiguratorMixin(object): wrapper=None, route_name=None, request_type=None, - request_method=None, + request_method=None, request_param=None, containment=None, xhr=None, @@ -1612,7 +1612,7 @@ class ViewsConfiguratorMixin(object): '%s may not be used as an argument to add_forbidden_view' % arg ) - + settings = dict( view=view, context=HTTPForbidden, @@ -1623,7 +1623,7 @@ class ViewsConfiguratorMixin(object): containment=containment, xhr=xhr, accept=accept, - header=header, + header=header, path_info=path_info, custom_predicates=custom_predicates, decorator=decorator, @@ -1638,7 +1638,7 @@ class ViewsConfiguratorMixin(object): return self.add_view(**settings) set_forbidden_view = add_forbidden_view # deprecated sorta-bw-compat alias - + @viewdefaults @action_method def add_notfound_view( @@ -1649,7 +1649,7 @@ class ViewsConfiguratorMixin(object): wrapper=None, route_name=None, request_type=None, - request_method=None, + request_method=None, request_param=None, containment=None, xhr=None, @@ -1700,7 +1700,7 @@ class ViewsConfiguratorMixin(object): '%s may not be used as an argument to add_notfound_view' % arg ) - + settings = dict( view=view, context=HTTPNotFound, @@ -1711,7 +1711,7 @@ class ViewsConfiguratorMixin(object): containment=containment, xhr=xhr, accept=accept, - header=header, + header=header, path_info=path_info, custom_predicates=custom_predicates, decorator=decorator, @@ -1786,7 +1786,15 @@ class ViewsConfiguratorMixin(object): ``Expires`` and ``Cache-Control`` headers for static assets served. Note that this argument has no effect when the ``name`` is a *url prefix*. By default, this argument is ``None``, meaning that no - particular Expires or Cache-Control headers are set in the response. + particular Expires or Cache-Control headers are set in the response, + unless ``cache_bust`` is specified. + + The ``cache_bust`` keyword argument may be set to ``"md5"`` to cause + :meth:`~pyramid.request.Request.static_url` to generate URLs with an + additional query string which includes the md5 checksum for the static + asset. This argument modifies the default for ``cache_max_age``, + making it ten years. ``cache_max_age`` may still be explicitly + provided to override this default. The ``permission`` keyword argument is used to specify the :term:`permission` required by a user to execute the static view. By -- cgit v1.2.3 From 5e61602652b2963ee0ef2df30ac81f29c42d3415 Mon Sep 17 00:00:00 2001 From: Alexey Torkhov Date: Mon, 14 Jul 2014 22:49:59 +0400 Subject: Update i18n.rst To set output file for pot-create -o flag should be used, not redirect. --- docs/narr/i18n.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst index 1de2c8941..cb2cd049c 100644 --- a/docs/narr/i18n.rst +++ b/docs/narr/i18n.rst @@ -326,7 +326,7 @@ application. You run a ``pot-create`` command to extract the messages: $ cd /place/where/myapplication/setup.py/lives $ mkdir -p myapplication/locale - $ $VENV/bin/pot-create src > myapplication/locale/myapplication.pot + $ $VENV/bin/pot-create -o myapplication/locale/myapplication.pot src The message catalog ``.pot`` template will end up in: -- cgit v1.2.3 From 0445bf2ac9c4cb7862464f1ce8f42c640c11ea7d Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Mon, 14 Jul 2014 15:59:05 -0400 Subject: Try this impl on and see how it feels. --- pyramid/cachebust.py | 34 +++++++++++++++++++++++++++++ pyramid/config/views.py | 19 ++++++++++++++--- pyramid/interfaces.py | 57 +++++++++++++++++++++++++++++++++++++++++++------ pyramid/static.py | 7 +++--- 4 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 pyramid/cachebust.py diff --git a/pyramid/cachebust.py b/pyramid/cachebust.py new file mode 100644 index 000000000..69c7eb1d2 --- /dev/null +++ b/pyramid/cachebust.py @@ -0,0 +1,34 @@ +import hashlib +import pkg_resources + +from zope.interface import implementer + +from .interfaces import ICacheBuster + +from pyramid.asset import resolve_asset_spec + + +def generate_md5(spec): + package, filename = resolve_asset_spec(spec) + md5 = hashlib.md5() + with pkg_resources.resource_stream(package, filename) as stream: + for block in iter(lambda: stream.read(4096), ''): + md5.update(block) + return md5.hexdigest() + + +@implementer(ICacheBuster) +class DefaultCacheBuster(object): + + def generate_token(self, request, pathspec): + token_cache = request.registry.setdefault('md5-token-cache', {}) + token = token_cache.get(pathspec) + if not token: + token_cache[pathspec] = token = generate_md5(pathspec) + return token + + def pregenerate_url(self, request, token, subpath, kw): + return token + '/' + subpath, kw + + def match_url(self, request, path_elements): + return path_elements[1:] diff --git a/pyramid/config/views.py b/pyramid/config/views.py index d938a7632..78c415b14 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -34,6 +34,7 @@ from pyramid.interfaces import ( ) from pyramid import renderers +from pyramid.cachebust import DefaultCacheBuster from pyramid.compat import ( string_types, @@ -1905,11 +1906,16 @@ class StaticURLInfo(object): registry = request.registry except AttributeError: # bw compat (for tests) registry = get_current_registry() - for (url, spec, route_name) in self._get_registrations(registry): + registrations = self._get_registrations(registry) + for (url, spec, route_name, cachebust) in registrations: if path.startswith(spec): subpath = path[len(spec):] if WIN: # pragma: no cover subpath = subpath.replace('\\', '/') # windows + if cachebust: + token = cachebust.generate_token(request, spec + subpath) + subpath, kw = cachebust.pregenerate_url( + request, token, subpath, kw) if url is None: kw['subpath'] = subpath return request.route_url(route_name, **kw) @@ -1949,6 +1955,10 @@ class StaticURLInfo(object): # make sure it ends with a slash name = name + '/' + cachebust = extra.pop('cachebust', None) + if cachebust is True: + cachebust = DefaultCacheBuster() + if url_parse(name).netloc: # it's a URL # url, spec, route_name @@ -1958,9 +1968,12 @@ class StaticURLInfo(object): # it's a view name url = None cache_max_age = extra.pop('cache_max_age', None) + if cache_max_age is None and cachebust: + cache_max_age = 10 * 365 * 24 * 60 * 60 # Ten(ish) years + # create a view view = static_view(spec, cache_max_age=cache_max_age, - use_subpath=True) + use_subpath=True, cachebust=cachebust) # Mutate extra to allow factory, etc to be passed through here. # Treat permission specially because we'd like to default to @@ -2001,7 +2014,7 @@ class StaticURLInfo(object): registrations.pop(idx) # url, spec, route_name - registrations.append((url, spec, route_name)) + registrations.append((url, spec, route_name, cachebust)) intr = config.introspectable('static views', name, diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index aa2dbdafd..e60898dbc 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -708,7 +708,7 @@ class IRoute(Interface): pregenerator = Attribute('This attribute should either be ``None`` or ' 'a callable object implementing the ' '``IRoutePregenerator`` interface') - + def match(path): """ If the ``path`` passed to this function can be matched by the @@ -803,7 +803,7 @@ class IContextURL(IResourceURL): # <__main__.Fudge object at 0x1cda890> # # <__main__.Another object at 0x1cda850> - + def virtual_root(): """ Return the virtual root related to a request and the current context""" @@ -837,9 +837,9 @@ class IPEP302Loader(Interface): def get_code(fullname): """ Return the code object for the module identified by 'fullname'. - + Return 'None' if it's a built-in or extension module. - + If the loader doesn't have the code object but it does have the source code, return the compiled source code. @@ -848,16 +848,16 @@ class IPEP302Loader(Interface): def get_source(fullname): """ Return the source code for the module identified by 'fullname'. - + Return a string, using newline characters for line endings, or None if the source is not available. - + Raise ImportError if the module can't be found by the importer at all. """ def get_filename(fullname): """ Return the value of '__file__' if the named module was loaded. - + If the module is not found, raise ImportError. """ @@ -1164,6 +1164,49 @@ class IJSONAdapter(Interface): class IPredicateList(Interface): """ Interface representing a predicate list """ +class ICacheBuster(Interface): + """ + An instance of a class which implements this interface may be passed as the + ``cachebust`` argument to + :meth:`pyramid.config.Configurator.add_static_view` to add cache busting + capability to a static view. + """ + def generate_token(request, pathspec): + """ + Return a token string for a static asset to be used to rewrite a + static asset URL for cache busting. + + The ``pathspec`` argument is the path specification for the asset we're + generating a token for. + """ + + def pregenerate_url(request, token, subpath, kw): + """ + Modifies the elements and/or keywords used to generate the URL for a + given static asset. + + The ``token`` argument is the result of calling + :meth:`~pyramid.interfaces.ICacheBuster.generate_token` for a static + asset. + + The ``subpath`` argument is the subpath in the static asset URL that + would normally be generated without cache busting. The ``kw`` + argument is the keywords dict that would be passed to + :meth:`~pyramid.request.Request.route_url`. + The return value should be a two-tuple of elements ``(subpath, kw)`` + which are modified from the incoming arguments. + """ + + def match_url(request, path_elements): + """ + Undo any modification to the subpath which may have been done by + :meth:`~pyramid.interfaces.ICacheBuster.pregenerate_url`. The + ``path_elements`` argument is a tuple of path elements that represent + the subpath of the asset request URL. The return value should be + a modified (or not) version of ``path_elements``, which will be used + ultimately to find the asset. + """ + # configuration phases: a lower phase number means the actions associated # with this phase will be executed earlier than those with later phase # numbers. The default phase number is 0, FTR. diff --git a/pyramid/static.py b/pyramid/static.py index aa67568d3..be191971a 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -78,7 +78,7 @@ class static_view(object): """ def __init__(self, root_dir, cache_max_age=3600, package_name=None, - use_subpath=False, index='index.html'): + use_subpath=False, index='index.html', cachebust=None): # package_name is for bw compat; it is preferred to pass in a # package-relative path as root_dir # (e.g. ``anotherpackage:foo/static``). @@ -91,13 +91,15 @@ class static_view(object): self.docroot = docroot self.norm_docroot = normcase(normpath(docroot)) self.index = index + self.cachebust = cachebust def __call__(self, context, request): if self.use_subpath: path_tuple = request.subpath else: path_tuple = traversal_path_info(request.environ['PATH_INFO']) - + if self.cachebust: + path_tuple = self.cachebust.match_url(request, path_tuple) path = _secure_path(path_tuple) if path is None: @@ -153,4 +155,3 @@ def _secure_path(path_tuple): return None encoded = slash.join(path_tuple) # will be unicode return encoded - -- cgit v1.2.3 From 4fcbd9e5351bb7ec417cb10ba89dc3af2a6ef9a7 Mon Sep 17 00:00:00 2001 From: Alexey Torkhov Date: Tue, 15 Jul 2014 12:35:13 +0400 Subject: Update i18n.rst msgfmt produces 'messages.mo' file in current dir by default, it needs -o to specify destination. Also, added ref to adding a translation directory. --- docs/narr/i18n.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst index cb2cd049c..6bfbc5136 100644 --- a/docs/narr/i18n.rst +++ b/docs/narr/i18n.rst @@ -402,11 +402,11 @@ command from Gettext: .. code-block:: text $ cd /place/where/myapplication/setup.py/lives - $ msgfmt myapplication/locale/*/LC_MESSAGES/*.po + $ msgfmt -o myapplication/locale/es/LC_MESSAGES/myapplication.mo myapplication/locale/es/LC_MESSAGES/myapplication.po This will create a ``.mo`` file for each ``.po`` file in your application. As long as the :term:`translation directory` in which -the ``.mo`` file ends up in is configured into your application, these +the ``.mo`` file ends up in is configured into your application (see :ref:`adding_a_translation_directory`), these translations will be available to :app:`Pyramid`. .. index:: -- cgit v1.2.3 From 9d521efce433af574382c86a7397f1ac53a73804 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Tue, 15 Jul 2014 09:56:28 -0400 Subject: Try something a little more decoupled and consistent. --- pyramid/cachebust.py | 34 --------------------- pyramid/config/views.py | 79 ++++++++++++++++++++++++++++++++++++++----------- pyramid/interfaces.py | 79 ++++++++++++++++++++++++++++--------------------- pyramid/static.py | 8 ++--- 4 files changed, 112 insertions(+), 88 deletions(-) delete mode 100644 pyramid/cachebust.py diff --git a/pyramid/cachebust.py b/pyramid/cachebust.py deleted file mode 100644 index 69c7eb1d2..000000000 --- a/pyramid/cachebust.py +++ /dev/null @@ -1,34 +0,0 @@ -import hashlib -import pkg_resources - -from zope.interface import implementer - -from .interfaces import ICacheBuster - -from pyramid.asset import resolve_asset_spec - - -def generate_md5(spec): - package, filename = resolve_asset_spec(spec) - md5 = hashlib.md5() - with pkg_resources.resource_stream(package, filename) as stream: - for block in iter(lambda: stream.read(4096), ''): - md5.update(block) - return md5.hexdigest() - - -@implementer(ICacheBuster) -class DefaultCacheBuster(object): - - def generate_token(self, request, pathspec): - token_cache = request.registry.setdefault('md5-token-cache', {}) - token = token_cache.get(pathspec) - if not token: - token_cache[pathspec] = token = generate_md5(pathspec) - return token - - def pregenerate_url(self, request, token, subpath, kw): - return token + '/' + subpath, kw - - def match_url(self, request, path_elements): - return path_elements[1:] diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 78c415b14..c09ddc73d 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1,6 +1,8 @@ +import hashlib import inspect import operator import os +import pkg_resources import warnings from zope.interface import ( @@ -34,7 +36,7 @@ from pyramid.interfaces import ( ) from pyramid import renderers -from pyramid.cachebust import DefaultCacheBuster +from pyramid.asset import resolve_asset_spec from pyramid.compat import ( string_types, @@ -45,11 +47,6 @@ from pyramid.compat import ( is_nonstr_iter ) -from pyramid.encode import ( - quote_plus, - urlencode, -) - from pyramid.exceptions import ( ConfigurationError, PredicateMismatch, @@ -1907,15 +1904,13 @@ class StaticURLInfo(object): except AttributeError: # bw compat (for tests) registry = get_current_registry() registrations = self._get_registrations(registry) - for (url, spec, route_name, cachebust) in registrations: + for (url, spec, route_name, cachebuster) in registrations: if path.startswith(spec): subpath = path[len(spec):] if WIN: # pragma: no cover subpath = subpath.replace('\\', '/') # windows - if cachebust: - token = cachebust.generate_token(request, spec + subpath) - subpath, kw = cachebust.pregenerate_url( - request, token, subpath, kw) + if cachebuster: + subpath, kw = cachebuster(subpath, kw) if url is None: kw['subpath'] = subpath return request.route_url(route_name, **kw) @@ -1955,9 +1950,22 @@ class StaticURLInfo(object): # make sure it ends with a slash name = name + '/' - cachebust = extra.pop('cachebust', None) - if cachebust is True: - cachebust = DefaultCacheBuster() + cb = extra.pop('cachebust', None) + if cb is True: + cb_token, cb_pregen, cb_match = DefaultCacheBuster() + elif cb: + cb_token, cb_pregen, cb_match = cb + else: + cb_token = cb_pregen = cb_match = None + + if cb_token and cb_pregen: + def cachebuster(subpath, kw): + token = cb_token(spec + subpath) + subpath_tuple = tuple(subpath.split('/')) + subpath_tuple, kw = cb_pregen(token, subpath_tuple, kw) + return '/'.join(subpath_tuple), kw + else: + cachebuster = None if url_parse(name).netloc: # it's a URL @@ -1968,12 +1976,12 @@ class StaticURLInfo(object): # it's a view name url = None cache_max_age = extra.pop('cache_max_age', None) - if cache_max_age is None and cachebust: + if cache_max_age is None and cb: cache_max_age = 10 * 365 * 24 * 60 * 60 # Ten(ish) years # create a view view = static_view(spec, cache_max_age=cache_max_age, - use_subpath=True, cachebust=cachebust) + use_subpath=True, cachebust_match=cb_match) # Mutate extra to allow factory, etc to be passed through here. # Treat permission specially because we'd like to default to @@ -2014,7 +2022,7 @@ class StaticURLInfo(object): registrations.pop(idx) # url, spec, route_name - registrations.append((url, spec, route_name, cachebust)) + registrations.append((url, spec, route_name, cachebuster)) intr = config.introspectable('static views', name, @@ -2026,3 +2034,40 @@ class StaticURLInfo(object): config.action(None, callable=register, introspectables=(intr,)) +def _generate_md5(spec): + package, filename = resolve_asset_spec(spec) + md5 = hashlib.md5() + with pkg_resources.resource_stream(package, filename) as stream: + for block in iter(lambda: stream.read(4096), ''): + md5.update(block) + return md5.hexdigest() + + +def DefaultCacheBuster(): + token_cache = {} + + def generate_token(pathspec): + # An astute observer will notice that this use of token_cache doesn't + # look particular thread safe. Basic read/write operations on Python + # dicts, however, are atomic, so simply accessing and writing values + # to the dict shouldn't cause a segfault or other catastrophic failure. + # (See: http://effbot.org/pyfaq/what-kinds-of-global-value-mutation-are-thread-safe.htm) + # + # We do have a race condition that could result in the same md5 + # checksum getting computed twice or more times in parallel. Since + # the program would still function just fine if this were to occur, + # the extra overhead of using locks to serialize access to the dict + # seems an unnecessary burden. + # + token = token_cache.get(pathspec) + if not token: + token_cache[pathspec] = token = _generate_md5(pathspec) + return token + + def pregenerate_url(token, subpath, kw): + return (token,) + subpath, kw + + def match_url(subpath): + return subpath[1:] + + return (generate_token, pregenerate_url, match_url) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index e60898dbc..84a6ad833 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1164,47 +1164,60 @@ class IJSONAdapter(Interface): class IPredicateList(Interface): """ Interface representing a predicate list """ -class ICacheBuster(Interface): - """ - An instance of a class which implements this interface may be passed as the - ``cachebust`` argument to - :meth:`pyramid.config.Configurator.add_static_view` to add cache busting - capability to a static view. - """ - def generate_token(request, pathspec): +class ICachebustTokenGenerator(Interface): + def __call__(pathspec): """ - Return a token string for a static asset to be used to rewrite a - static asset URL for cache busting. + A function which computes and returns a token string used for cache + busting. ``pathspec`` is the path specification for the resource to be + cache busted. Often a cachebust token might be computed for a specific + asset (e.g. an md5 checksum), but probably just as often people use + schemes where a single cachebust token is used globally. It could be a + git commit sha1, a timestamp, or something configured manually. A + pattern that can be useful is to use to a factory function and a + closure to return a function that depends on some configuration. For + example: - The ``pathspec`` argument is the path specification for the asset we're - generating a token for. + .. code-block:: python + :linenos: + + def use_configured_cachebust_token(config): + # config is an instance of pyramid.config.Configurator + token = config.registry.settings['myapp.cachebust_token'] + def cachebust_token(pathspec): + return token + return cachebust_token """ - def pregenerate_url(request, token, subpath, kw): +class ICachebustURLPregenerator(Interface): + def __call__(token, subpath, kw): """ - Modifies the elements and/or keywords used to generate the URL for a - given static asset. - - The ``token`` argument is the result of calling - :meth:`~pyramid.interfaces.ICacheBuster.generate_token` for a static - asset. - - The ``subpath`` argument is the subpath in the static asset URL that - would normally be generated without cache busting. The ``kw`` - argument is the keywords dict that would be passed to - :meth:`~pyramid.request.Request.route_url`. - The return value should be a two-tuple of elements ``(subpath, kw)`` - which are modified from the incoming arguments. + A function which modifies a subpath and/or keyword arguments from which + a static asset URL will be computed during URL generation. The + ``token`` argument is a token string computed by an instance of + :class:`~pyramid.interfaces.ICachebustTokenGenerator` for a particular + asset. The ``subpath`` argument is a tuple of path elements that + represent the portion of the asset URL which is used to find the asset. + The ``kw`` argument is a dict of keywords that are to be passed + eventually to :meth:`~pyramid.request.Request.route_url` for URL + generation. The return value of this function should be two-tuple of + ``(subpath, kw)`` which are versions of the same arguments modified to + include the cachebust token in the generated URL. """ - def match_url(request, path_elements): +class ICachebustURLMatcher(Interface): + def __call__(subpath): """ - Undo any modification to the subpath which may have been done by - :meth:`~pyramid.interfaces.ICacheBuster.pregenerate_url`. The - ``path_elements`` argument is a tuple of path elements that represent - the subpath of the asset request URL. The return value should be - a modified (or not) version of ``path_elements``, which will be used - ultimately to find the asset. + A function which performs the logical inverse of an + :class:`~pyramid.interfaces.ICacheBustURLPregenerator`, by taking a + subpath from a cache busted URL and removing the cachebust token, so + that :app:`Pyramid` can find the underlying asset. If the cache + busting scheme in use doesn't specifically modify the path portion of + the generated URL (e.g. it adds a query string), a function which + implements this interface may not be necessary. + + ``subpath`` is the subpath portion of the URL for an incoming request + for a static asset. The return value should be the same tuple with the + cache busting token elided. """ # configuration phases: a lower phase number means the actions associated diff --git a/pyramid/static.py b/pyramid/static.py index be191971a..87bbcd34c 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -78,7 +78,7 @@ class static_view(object): """ def __init__(self, root_dir, cache_max_age=3600, package_name=None, - use_subpath=False, index='index.html', cachebust=None): + use_subpath=False, index='index.html', cachebust_match=None): # package_name is for bw compat; it is preferred to pass in a # package-relative path as root_dir # (e.g. ``anotherpackage:foo/static``). @@ -91,15 +91,15 @@ class static_view(object): self.docroot = docroot self.norm_docroot = normcase(normpath(docroot)) self.index = index - self.cachebust = cachebust + self.cachebust_match = cachebust_match def __call__(self, context, request): if self.use_subpath: path_tuple = request.subpath else: path_tuple = traversal_path_info(request.environ['PATH_INFO']) - if self.cachebust: - path_tuple = self.cachebust.match_url(request, path_tuple) + if self.cachebust_match: + path_tuple = self.cachebust_match(path_tuple) path = _secure_path(path_tuple) if path is None: -- cgit v1.2.3 From 9af33504d9d621bc0f87752837a09f9110e454e5 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Tue, 15 Jul 2014 10:52:14 -0400 Subject: Show an example. --- pyramid/interfaces.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 84a6ad833..95aa1d60e 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1202,6 +1202,15 @@ class ICachebustURLPregenerator(Interface): generation. The return value of this function should be two-tuple of ``(subpath, kw)`` which are versions of the same arguments modified to include the cachebust token in the generated URL. + + Here is an example which places the token in a query string: + + .. code-block:: python + :linenos: + + def cb_pregen(token, subpath kw): + kw.setdefault('_query', {})['cb'] = token + return subpath, kw """ class ICachebustURLMatcher(Interface): -- cgit v1.2.3 From de2996ddcc7c2ac5c3e59101df0fed1ab832701b Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Tue, 15 Jul 2014 10:57:36 -0400 Subject: Make sure it's possible to still set cache_max_age to None even if cache busting is being used. --- pyramid/config/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index c09ddc73d..b583b59a0 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1975,9 +1975,9 @@ class StaticURLInfo(object): else: # it's a view name url = None - cache_max_age = extra.pop('cache_max_age', None) - if cache_max_age is None and cb: - cache_max_age = 10 * 365 * 24 * 60 * 60 # Ten(ish) years + ten_years = 10 * 365 * 24 * 60 * 60 # more or less + default = ten_years if cb else None + cache_max_age = extra.pop('cache_max_age', default) # create a view view = static_view(spec, cache_max_age=cache_max_age, -- cgit v1.2.3 From 2a1ca8c542e752bdd1de2bfdac0f3365a209c072 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Tue, 15 Jul 2014 14:19:07 -0400 Subject: I kind of like Raydeo's last idea. --- pyramid/config/views.py | 62 ++++++++++--------------------------------------- pyramid/interfaces.py | 29 +++++++++++++---------- pyramid/static.py | 56 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 62 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index b583b59a0..4b7bdaa81 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1,8 +1,6 @@ -import hashlib import inspect import operator import os -import pkg_resources import warnings from zope.interface import ( @@ -36,7 +34,10 @@ from pyramid.interfaces import ( ) from pyramid import renderers -from pyramid.asset import resolve_asset_spec +from pyramid.static import ( + Md5AssetTokenGenerator, + PathSegmentCacheBuster, +) from pyramid.compat import ( string_types, @@ -1950,19 +1951,14 @@ class StaticURLInfo(object): # make sure it ends with a slash name = name + '/' - cb = extra.pop('cachebust', None) + cb = extra.pop('cachebuster', None) if cb is True: - cb_token, cb_pregen, cb_match = DefaultCacheBuster() - elif cb: - cb_token, cb_pregen, cb_match = cb - else: - cb_token = cb_pregen = cb_match = None - - if cb_token and cb_pregen: + cb = DefaultCacheBuster() + if cb: def cachebuster(subpath, kw): - token = cb_token(spec + subpath) + token = cb.token(spec + subpath) subpath_tuple = tuple(subpath.split('/')) - subpath_tuple, kw = cb_pregen(token, subpath_tuple, kw) + subpath_tuple, kw = cb.pregenerate(token, subpath_tuple, kw) return '/'.join(subpath_tuple), kw else: cachebuster = None @@ -1980,6 +1976,7 @@ class StaticURLInfo(object): cache_max_age = extra.pop('cache_max_age', default) # create a view + cb_match = getattr(cb, 'match', None) view = static_view(spec, cache_max_age=cache_max_age, use_subpath=True, cachebust_match=cb_match) @@ -2033,41 +2030,6 @@ class StaticURLInfo(object): config.action(None, callable=register, introspectables=(intr,)) - -def _generate_md5(spec): - package, filename = resolve_asset_spec(spec) - md5 = hashlib.md5() - with pkg_resources.resource_stream(package, filename) as stream: - for block in iter(lambda: stream.read(4096), ''): - md5.update(block) - return md5.hexdigest() - - def DefaultCacheBuster(): - token_cache = {} - - def generate_token(pathspec): - # An astute observer will notice that this use of token_cache doesn't - # look particular thread safe. Basic read/write operations on Python - # dicts, however, are atomic, so simply accessing and writing values - # to the dict shouldn't cause a segfault or other catastrophic failure. - # (See: http://effbot.org/pyfaq/what-kinds-of-global-value-mutation-are-thread-safe.htm) - # - # We do have a race condition that could result in the same md5 - # checksum getting computed twice or more times in parallel. Since - # the program would still function just fine if this were to occur, - # the extra overhead of using locks to serialize access to the dict - # seems an unnecessary burden. - # - token = token_cache.get(pathspec) - if not token: - token_cache[pathspec] = token = _generate_md5(pathspec) - return token - - def pregenerate_url(token, subpath, kw): - return (token,) + subpath, kw - - def match_url(subpath): - return subpath[1:] - - return (generate_token, pregenerate_url, match_url) + return PathSegmentCacheBuster(Md5AssetTokenGenerator()) + diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 95aa1d60e..822d1624c 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1164,8 +1164,12 @@ class IJSONAdapter(Interface): class IPredicateList(Interface): """ Interface representing a predicate list """ -class ICachebustTokenGenerator(Interface): - def __call__(pathspec): +class ICacheBuster(Interface): + """ + A container for functions which implement a cache busting policy for + serving static assets. + """ + def token(pathspec): """ A function which computes and returns a token string used for cache busting. ``pathspec`` is the path specification for the resource to be @@ -1188,13 +1192,12 @@ class ICachebustTokenGenerator(Interface): return cachebust_token """ -class ICachebustURLPregenerator(Interface): - def __call__(token, subpath, kw): + def pregenerate(token, subpath, kw): """ A function which modifies a subpath and/or keyword arguments from which a static asset URL will be computed during URL generation. The ``token`` argument is a token string computed by an instance of - :class:`~pyramid.interfaces.ICachebustTokenGenerator` for a particular + :method:`~pyramid.interfaces.ICacheBuster.token` for a particular asset. The ``subpath`` argument is a tuple of path elements that represent the portion of the asset URL which is used to find the asset. The ``kw`` argument is a dict of keywords that are to be passed @@ -1213,20 +1216,22 @@ class ICachebustURLPregenerator(Interface): return subpath, kw """ -class ICachebustURLMatcher(Interface): - def __call__(subpath): + def match(subpath): """ A function which performs the logical inverse of an - :class:`~pyramid.interfaces.ICacheBustURLPregenerator`, by taking a + :method:`~pyramid.interfaces.ICacheBuster.pregenerate`, by taking a subpath from a cache busted URL and removing the cachebust token, so - that :app:`Pyramid` can find the underlying asset. If the cache - busting scheme in use doesn't specifically modify the path portion of - the generated URL (e.g. it adds a query string), a function which - implements this interface may not be necessary. + that :app:`Pyramid` can find the underlying asset. ``subpath`` is the subpath portion of the URL for an incoming request for a static asset. The return value should be the same tuple with the cache busting token elided. + + If the cache busting scheme in use doesn't specifically modify the path + portion of the generated URL (e.g. it adds a query string), a function + which implements this interface may not be necessary. It is + permissible for an instance of + :class:`~pyramid.interfaces.ICacheBuster` to omit this function. """ # configuration phases: a lower phase number means the actions associated diff --git a/pyramid/static.py b/pyramid/static.py index 87bbcd34c..92251721e 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import hashlib import os +import pkg_resources from os.path import ( normcase, @@ -155,3 +157,57 @@ def _secure_path(path_tuple): return None encoded = slash.join(path_tuple) # will be unicode return encoded + +def _generate_md5(spec): + package, filename = resolve_asset_spec(spec) + md5 = hashlib.md5() + with pkg_resources.resource_stream(package, filename) as stream: + for block in iter(lambda: stream.read(4096), ''): + md5.update(block) + return md5.hexdigest() + +def Md5AssetTokenGenerator(): + token_cache = {} + + def generate_token(pathspec): + # An astute observer will notice that this use of token_cache doesn't + # look particularly thread safe. Basic read/write operations on Python + # dicts, however, are atomic, so simply accessing and writing values + # to the dict shouldn't cause a segfault or other catastrophic failure. + # (See: http://effbot.org/pyfaq/what-kinds-of-global-value-mutation-are-thread-safe.htm) + # + # We do have a race condition that could result in the same md5 + # checksum getting computed twice or more times in parallel. Since + # the program would still function just fine if this were to occur, + # the extra overhead of using locks to serialize access to the dict + # seems an unnecessary burden. + # + token = token_cache.get(pathspec) + if not token: + token_cache[pathspec] = token = _generate_md5(pathspec) + return token + + return generate_token + +class PathSegmentCacheBuster(object): + + def __init__(self, token): + self.token = token + + def pregenerate(self, token, subpath, kw): + return (token,) + subpath, kw + + def match(self, subpath): + return subpath[1:] + +class QueryStringCacheBuster(object): + + def __init__(self, token, param='x'): + self.param = param + self.token = token + + def pregenerate(self, token, subpath, kw): + kw.setdefault('_query', {})[self.param] = token + return subpath, kw + + -- cgit v1.2.3 From cac23bb790da283fad7ad51ac4c18fc3903ebb92 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Tue, 15 Jul 2014 16:52:15 -0400 Subject: Fix broken tests. --- pyramid/tests/test_config/test_views.py | 45 +++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 57bb5e9d0..e01aed1f2 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -113,7 +113,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.add_view(renderer='dummy.pt') view = self._getViewCallable(config) self.assertRaises(ValueError, view, None, None) - + def test_add_view_with_tmpl_renderer_factory_no_renderer_factory(self): config = self._makeOne(autocommit=True) introspector = DummyIntrospector() @@ -136,7 +136,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): ('renderer factories', '.pt') in introspector.related[-1]) view = self._getViewCallable(config) self.assertTrue(b'Hello!' in view(None, None).body) - + def test_add_view_wrapped_view_is_decorated(self): def view(request): # request-only wrapper """ """ @@ -3742,8 +3742,9 @@ class TestStaticURLInfo(unittest.TestCase): def test_generate_registration_miss(self): inst = self._makeOne() - registrations = [(None, 'spec', 'route_name'), - ('http://example.com/foo/', 'package:path/', None)] + registrations = [ + (None, 'spec', 'route_name', None), + ('http://example.com/foo/', 'package:path/', None, None)] inst._get_registrations = lambda *x: registrations request = self._makeRequest() result = inst.generate('package:path/abc', request) @@ -3751,7 +3752,8 @@ class TestStaticURLInfo(unittest.TestCase): def test_generate_registration_no_registry_on_request(self): inst = self._makeOne() - registrations = [('http://example.com/foo/', 'package:path/', None)] + registrations = [ + ('http://example.com/foo/', 'package:path/', None, None)] inst._get_registrations = lambda *x: registrations request = self._makeRequest() del request.registry @@ -3760,7 +3762,8 @@ class TestStaticURLInfo(unittest.TestCase): def test_generate_slash_in_name1(self): inst = self._makeOne() - registrations = [('http://example.com/foo/', 'package:path/', None)] + registrations = [ + ('http://example.com/foo/', 'package:path/', None, None)] inst._get_registrations = lambda *x: registrations request = self._makeRequest() result = inst.generate('package:path/abc', request) @@ -3768,7 +3771,8 @@ class TestStaticURLInfo(unittest.TestCase): def test_generate_slash_in_name2(self): inst = self._makeOne() - registrations = [('http://example.com/foo/', 'package:path/', None)] + registrations = [ + ('http://example.com/foo/', 'package:path/', None, None)] inst._get_registrations = lambda *x: registrations request = self._makeRequest() result = inst.generate('package:path/', request) @@ -3788,7 +3792,7 @@ class TestStaticURLInfo(unittest.TestCase): def test_generate_route_url(self): inst = self._makeOne() - registrations = [(None, 'package:path/', '__viewname/')] + registrations = [(None, 'package:path/', '__viewname/', None)] inst._get_registrations = lambda *x: registrations def route_url(n, **kw): self.assertEqual(n, '__viewname/') @@ -3801,7 +3805,7 @@ class TestStaticURLInfo(unittest.TestCase): def test_generate_url_unquoted_local(self): inst = self._makeOne() - registrations = [(None, 'package:path/', '__viewname/')] + registrations = [(None, 'package:path/', '__viewname/', None)] inst._get_registrations = lambda *x: registrations def route_url(n, **kw): self.assertEqual(n, '__viewname/') @@ -3814,7 +3818,7 @@ class TestStaticURLInfo(unittest.TestCase): def test_generate_url_quoted_remote(self): inst = self._makeOne() - registrations = [('http://example.com/', 'package:path/', None)] + registrations = [('http://example.com/', 'package:path/', None, None)] inst._get_registrations = lambda *x: registrations request = self._makeRequest() result = inst.generate('package:path/abc def', request, a=1) @@ -3822,7 +3826,7 @@ class TestStaticURLInfo(unittest.TestCase): def test_generate_url_with_custom_query(self): inst = self._makeOne() - registrations = [('http://example.com/', 'package:path/', None)] + registrations = [('http://example.com/', 'package:path/', None, None)] inst._get_registrations = lambda *x: registrations request = self._makeRequest() result = inst.generate('package:path/abc def', request, a=1, @@ -3832,7 +3836,7 @@ class TestStaticURLInfo(unittest.TestCase): def test_generate_url_with_custom_anchor(self): inst = self._makeOne() - registrations = [('http://example.com/', 'package:path/', None)] + registrations = [('http://example.com/', 'package:path/', None, None)] inst._get_registrations = lambda *x: registrations request = self._makeRequest() uc = text_(b'La Pe\xc3\xb1a', 'utf-8') @@ -3846,28 +3850,31 @@ class TestStaticURLInfo(unittest.TestCase): config = self._makeConfig( [('http://example.com/', 'package:path/', None)]) inst.add(config, 'http://example.com', 'anotherpackage:path') - expected = [('http://example.com/', 'anotherpackage:path/', None)] + expected = [ + ('http://example.com/', 'anotherpackage:path/', None, None)] self._assertRegistrations(config, expected) def test_add_url_withendslash(self): inst = self._makeOne() config = self._makeConfig() inst.add(config, 'http://example.com/', 'anotherpackage:path') - expected = [('http://example.com/', 'anotherpackage:path/', None)] + expected = [ + ('http://example.com/', 'anotherpackage:path/', None, None)] self._assertRegistrations(config, expected) def test_add_url_noendslash(self): inst = self._makeOne() config = self._makeConfig() inst.add(config, 'http://example.com', 'anotherpackage:path') - expected = [('http://example.com/', 'anotherpackage:path/', None)] + expected = [ + ('http://example.com/', 'anotherpackage:path/', None, 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)] + expected = [('//example.com/', 'anotherpackage:path/', None, None)] self._assertRegistrations(config, expected) def test_add_viewname(self): @@ -3876,7 +3883,7 @@ class TestStaticURLInfo(unittest.TestCase): config = self._makeConfig() inst = self._makeOne() inst.add(config, 'view', 'anotherpackage:path', cache_max_age=1) - expected = [(None, 'anotherpackage:path/', '__view/')] + expected = [(None, 'anotherpackage:path/', '__view/', None)] self._assertRegistrations(config, expected) self.assertEqual(config.route_args, ('__view/', 'view/*subpath')) self.assertEqual(config.view_kw['permission'], NO_PERMISSION_REQUIRED) @@ -3887,7 +3894,7 @@ class TestStaticURLInfo(unittest.TestCase): config.route_prefix = '/abc' inst = self._makeOne() inst.add(config, 'view', 'anotherpackage:path',) - expected = [(None, 'anotherpackage:path/', '__/abc/view/')] + expected = [(None, 'anotherpackage:path/', '__/abc/view/', None)] self._assertRegistrations(config, expected) self.assertEqual(config.route_args, ('__/abc/view/', 'view/*subpath')) @@ -3904,7 +3911,7 @@ class TestStaticURLInfo(unittest.TestCase): inst.add(config, 'view', 'anotherpackage:path', cache_max_age=1, context=DummyContext) self.assertEqual(config.view_kw['context'], DummyContext) - + def test_add_viewname_with_for_(self): config = self._makeConfig() inst = self._makeOne() -- cgit v1.2.3 From 5350158f666a638293bd2b3d7cd19029e0bab145 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Tue, 15 Jul 2014 18:02:01 -0400 Subject: Test coverage for pyramid.config.views --- pyramid/config/views.py | 11 ++++++---- pyramid/tests/test_config/test_views.py | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 4b7bdaa81..00c5622e7 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1891,6 +1891,12 @@ def isexception(o): @implementer(IStaticURLInfo) class StaticURLInfo(object): + # Indirection for testing + _default_cachebuster = staticmethod(PathSegmentCacheBuster) + _default_asset_token_generator = staticmethod(Md5AssetTokenGenerator) + + def _make_default_cachebuster(self): + return self._default_cachebuster(self._default_asset_token_generator()) def _get_registrations(self, registry): try: @@ -1953,7 +1959,7 @@ class StaticURLInfo(object): cb = extra.pop('cachebuster', None) if cb is True: - cb = DefaultCacheBuster() + cb = self._make_default_cachebuster() if cb: def cachebuster(subpath, kw): token = cb.token(spec + subpath) @@ -2030,6 +2036,3 @@ class StaticURLInfo(object): config.action(None, callable=register, introspectables=(intr,)) -def DefaultCacheBuster(): - return PathSegmentCacheBuster(Md5AssetTokenGenerator()) - diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index e01aed1f2..0b81f5a6f 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3845,6 +3845,20 @@ class TestStaticURLInfo(unittest.TestCase): self.assertEqual(result, 'http://example.com/abc%20def#La%20Pe%C3%B1a') + def test_generate_url_cachebuster(self): + def cachebuster(subpath, kw): + kw['foo'] = 'bar' + return 'foo' + '/' + subpath, kw + inst = self._makeOne() + registrations = [(None, 'package:path/', '__viewname', cachebuster)] + inst._get_registrations = lambda *x: registrations + request = self._makeRequest() + def route_url(n, **kw): + self.assertEqual(n, '__viewname') + self.assertEqual(kw, {'subpath':'foo/abc', 'foo':'bar'}) + request.route_url = route_url + inst.generate('package:path/abc', request) + def test_add_already_exists(self): inst = self._makeOne() config = self._makeConfig( @@ -3927,6 +3941,31 @@ class TestStaticURLInfo(unittest.TestCase): self.assertEqual(config.view_kw['renderer'], 'mypackage:templates/index.pt') + def test_add_cachebust_default(self): + config = self._makeConfig() + inst = self._makeOne() + inst._default_asset_token_generator = lambda: lambda pathspec: 'foo' + inst.add(config, 'view', 'mypackage:path', cachebuster=True) + cachebuster = config.registry._static_url_registrations[0][3] + subpath, _ = cachebuster('some/path', None) + self.assertEqual(subpath, 'foo/some/path') + + def test_add_cachebust_custom(self): + class DummyCacheBuster(object): + def token(self, pathspec): + return 'foo' + def pregenerate(self, token, subpath, kw): + kw['x'] = token + return subpath, kw + config = self._makeConfig() + inst = self._makeOne() + inst.add(config, 'view', 'mypackage:path', + cachebuster=DummyCacheBuster()) + cachebuster = config.registry._static_url_registrations[0][3] + subpath, kw = cachebuster('some/path', {}) + self.assertEqual(subpath, 'some/path') + self.assertEqual(kw['x'], 'foo') + class Test_view_description(unittest.TestCase): def _callFUT(self, view): from pyramid.config.views import view_description -- cgit v1.2.3 From dc97173e0c7306792814e3fa44dc0cd8e0e493b9 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Wed, 16 Jul 2014 09:10:25 -0400 Subject: Make sure md5 checksum works for non-package assets. --- pyramid/static.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyramid/static.py b/pyramid/static.py index 92251721e..290732640 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -160,8 +160,12 @@ def _secure_path(path_tuple): def _generate_md5(spec): package, filename = resolve_asset_spec(spec) + if package: + stream = pkg_resources.resource_stream(package, filename) + else: + stream = open(filename, 'rb') md5 = hashlib.md5() - with pkg_resources.resource_stream(package, filename) as stream: + with stream: for block in iter(lambda: stream.read(4096), ''): md5.update(block) return md5.hexdigest() -- cgit v1.2.3 From 46c0294c5e66712e186de96f55ced580d3ae4c0b Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Wed, 16 Jul 2014 10:42:37 -0400 Subject: Use the framework, Luke. --- pyramid/static.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pyramid/static.py b/pyramid/static.py index 290732640..7616b0a29 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -28,7 +28,7 @@ from pyramid.httpexceptions import ( HTTPMovedPermanently, ) -from pyramid.path import caller_package +from pyramid.path import AssetResolver, caller_package from pyramid.response import FileResponse from pyramid.traversal import traversal_path_info @@ -159,13 +159,9 @@ def _secure_path(path_tuple): return encoded def _generate_md5(spec): - package, filename = resolve_asset_spec(spec) - if package: - stream = pkg_resources.resource_stream(package, filename) - else: - stream = open(filename, 'rb') + asset = AssetResolver(None).resolve(spec) md5 = hashlib.md5() - with stream: + with asset.stream() as stream: for block in iter(lambda: stream.read(4096), ''): md5.update(block) return md5.hexdigest() -- cgit v1.2.3 From e7339162285144e7bfd716e1e4e000f34974b1c2 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Wed, 16 Jul 2014 10:44:55 -0400 Subject: Unused import. --- pyramid/static.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyramid/static.py b/pyramid/static.py index 7616b0a29..9d691ca46 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import hashlib import os -import pkg_resources from os.path import ( normcase, -- cgit v1.2.3 From faaed6c7cffb453aed823b80f4169e87bfbc8026 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 16 Jul 2014 10:28:47 -0500 Subject: remove mako docs that should be in pyramid_mako package --- docs/narr/environment.rst | 148 ---------------------------------------------- 1 file changed, 148 deletions(-) diff --git a/docs/narr/environment.rst b/docs/narr/environment.rst index 412635f08..7bac12ea7 100644 --- a/docs/narr/environment.rst +++ b/docs/narr/environment.rst @@ -13,7 +13,6 @@ single: reload settings single: default_locale_name single: environment variables - single: Mako environment settings single: ini file settings single: PasteDeploy settings @@ -396,153 +395,6 @@ Is equivalent to using the following statements in your configuration code: It is fine to use both or either form. -.. _mako_template_renderer_settings: - -Mako Template Render Settings ------------------------------ - -Mako derives additional settings to configure its template renderer that -should be set when using it. Many of these settings are optional and only need -to be set if they should be different from the default. The Mako Template -Renderer uses a subclass of Mako's `template lookup -`_ and accepts -several arguments to configure it. - -Mako Directories -~~~~~~~~~~~~~~~~ - -The value(s) supplied here are passed in as the template directories. They -should be in :term:`asset specification` format, for example: -``my.package:templates``. - -+-----------------------------+ -| Config File Setting Name | -+=============================+ -| ``mako.directories`` | -| | -| | -| | -+-----------------------------+ - -Mako Module Directory -~~~~~~~~~~~~~~~~~~~~~ - -The value supplied here tells Mako where to store compiled Mako templates. If -omitted, compiled templates will be stored in memory. This value should be an -absolute path, for example: ``%(here)s/data/templates`` would use a directory -called ``data/templates`` in the same parent directory as the INI file. - -+-----------------------------+ -| Config File Setting Name | -+=============================+ -| ``mako.module_directory`` | -| | -| | -| | -+-----------------------------+ - -Mako Input Encoding -~~~~~~~~~~~~~~~~~~~ - -The encoding that Mako templates are assumed to have. By default this is set -to ``utf-8``. If you wish to use a different template encoding, this value -should be changed accordingly. - -+-----------------------------+ -| Config File Setting Name | -+=============================+ -| ``mako.input_encoding`` | -| | -| | -| | -+-----------------------------+ - -Mako Error Handler -~~~~~~~~~~~~~~~~~~ - -A callable (or a :term:`dotted Python name` which names a callable) which is -called whenever Mako compile or runtime exceptions occur. The callable is -passed the current context as well as the exception. If the callable returns -True, the exception is considered to be handled, else it is re-raised after -the function completes. Is used to provide custom error-rendering functions. - -+-----------------------------+ -| Config File Setting Name | -+=============================+ -| ``mako.error_handler`` | -| | -| | -| | -+-----------------------------+ - -Mako Default Filters -~~~~~~~~~~~~~~~~~~~~ - -List of string filter names that will be applied to all Mako expressions. - -+-----------------------------+ -| Config File Setting Name | -+=============================+ -| ``mako.default_filters`` | -| | -| | -| | -+-----------------------------+ - -Mako Import -~~~~~~~~~~~ - -String list of Python statements, typically individual "import" lines, which -will be placed into the module level preamble of all generated Python modules. - - -+-----------------------------+ -| Config File Setting Name | -+=============================+ -| ``mako.imports`` | -| | -| | -| | -+-----------------------------+ - - -Mako Strict Undefined -~~~~~~~~~~~~~~~~~~~~~ - -``true`` or ``false``, representing the "strict undefined" behavior of Mako -(see `Mako Context Variables -`_). By -default, this is ``false``. - -+-----------------------------+ -| Config File Setting Name | -+=============================+ -| ``mako.strict_undefined`` | -| | -| | -| | -+-----------------------------+ - -Mako Preprocessor -~~~~~~~~~~~~~~~~~ - -.. versionadded:: 1.1 - -A callable (or a :term:`dotted Python name` which names a callable) which is -called to preprocess the source before the template is called. The callable -will be passed the full template source before it is parsed. The return -result of the callable will be used as the template source code. - - -+-----------------------------+ -| Config File Setting Name | -+=============================+ -| ``mako.preprocessor`` | -| | -| | -| | -+-----------------------------+ - Examples -------- -- cgit v1.2.3 From b4245a312bfe7f99080d46c1f9814f2c5da2cbf1 Mon Sep 17 00:00:00 2001 From: nick knouf Date: Tue, 15 Jul 2014 17:04:07 -0400 Subject: Updating to current msginit syntax --- docs/narr/i18n.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst index 6bfbc5136..95f663584 100644 --- a/docs/narr/i18n.rst +++ b/docs/narr/i18n.rst @@ -352,7 +352,7 @@ command from Gettext: $ cd /place/where/myapplication/setup.py/lives $ cd myapplication/locale $ mkdir -p es/LC_MESSAGES - $ msginit -l es es/LC_MESSAGES/myapplication.po + $ msginit -l es -o es/LC_MESSAGES/myapplication.po This will create a new the message catalog ``.po`` file will in: -- cgit v1.2.3 From eac3ff43f78a33d05e634cd5b4866f7681db34c3 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Wed, 16 Jul 2014 17:22:57 -0400 Subject: Test coverage for static. --- pyramid/tests/test_static.py | 99 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 96 insertions(+), 3 deletions(-) diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 94497d4f6..5edb70b50 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -26,7 +26,7 @@ class Test_static_view_use_subpath_False(unittest.TestCase): if kw is not None: environ.update(kw) return Request(environ=environ) - + def test_ctor_defaultargs(self): inst = self._makeOne('package:resource_name') self.assertEqual(inst.package_name, 'package') @@ -110,6 +110,14 @@ class Test_static_view_use_subpath_False(unittest.TestCase): response = inst(context, request) self.assertTrue(b'static' in response.body) + def test_cachebust_match(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + inst.cachebust_match = lambda subpath: subpath[1:] + request = self._makeRequest({'PATH_INFO':'/foo/index.html'}) + context = DummyContext() + response = inst(context, request) + self.assertTrue(b'static' in response.body) + def test_resource_is_file_with_wsgi_file_wrapper(self): from pyramid.response import _BLOCK_SIZE inst = self._makeOne('pyramid.tests:fixtures/static') @@ -218,7 +226,7 @@ class Test_static_view_use_subpath_True(unittest.TestCase): if kw is not None: environ.update(kw) return Request(environ=environ) - + def test_ctor_defaultargs(self): inst = self._makeOne('package:resource_name') self.assertEqual(inst.package_name, 'package') @@ -273,7 +281,7 @@ class Test_static_view_use_subpath_True(unittest.TestCase): context = DummyContext() from pyramid.httpexceptions import HTTPNotFound self.assertRaises(HTTPNotFound, inst, context, request) - + def test_oob_os_sep(self): import os inst = self._makeOne('pyramid.tests:fixtures/static') @@ -360,6 +368,91 @@ class Test_static_view_use_subpath_True(unittest.TestCase): from pyramid.httpexceptions import HTTPNotFound self.assertRaises(HTTPNotFound, inst, context, request) +class TestMd5AssetTokenGenerator(unittest.TestCase): + + def setUp(self): + import os + import tempfile + self.tmp = tempfile.mkdtemp() + self.fspath = os.path.join(self.tmp, 'test.txt') + + def tearDown(self): + import shutil + shutil.rmtree(self.tmp) + + def _makeOne(self): + from pyramid.static import Md5AssetTokenGenerator as unit + return unit() + + def test_package_resource(self): + fut = self._makeOne() + expected = '76d653a3a044e2f4b38bb001d283e3d9' + token = fut('pyramid.tests:fixtures/static/index.html') + self.assertEqual(token, expected) + + def test_filesystem_resource(self): + fut = self._makeOne() + expected = 'd5155f250bef0e9923e894dbc713c5dd' + with open(self.fspath, 'w') as f: + f.write("Are we rich yet?") + token = fut(self.fspath) + self.assertEqual(token, expected) + + def test_cache(self): + fut = self._makeOne() + expected = 'd5155f250bef0e9923e894dbc713c5dd' + with open(self.fspath, 'w') as f: + f.write("Are we rich yet?") + token = fut(self.fspath) + self.assertEqual(token, expected) + + # md5 shouldn't change because we've cached it + with open(self.fspath, 'w') as f: + f.write("Sorry for the convenience.") + token = fut(self.fspath) + self.assertEqual(token, expected) + +class TestPathSegmentCacheBuster(unittest.TestCase): + + def _makeOne(self): + from pyramid.static import PathSegmentCacheBuster as unit + return unit(lambda pathspec: 'foo') + + def test_token(self): + fut = self._makeOne().token + self.assertEqual(fut('whatever'), 'foo') + + def test_pregenerate(self): + fut = self._makeOne().pregenerate + self.assertEqual(fut('foo', ('bar',), 'kw'), (('foo', 'bar'), 'kw')) + + def test_match(self): + fut = self._makeOne().match + self.assertEqual(fut(('foo', 'bar')), ('bar',)) + +class TestQueryStringCacheBuster(unittest.TestCase): + + def _makeOne(self): + from pyramid.static import QueryStringCacheBuster as unit + return unit(lambda pathspec: 'foo') + + def test_token(self): + fut = self._makeOne().token + self.assertEqual(fut('whatever'), 'foo') + + def test_pregenerate(self): + fut = self._makeOne().pregenerate + self.assertEqual( + fut('foo', ('bar',), {}), + (('bar',), {'_query': {'x': 'foo'}})) + + def test_pregenerate_change_param(self): + from pyramid.static import QueryStringCacheBuster as unit + fut = unit(lambda pathspec: 'foo', 'y').pregenerate + self.assertEqual( + fut('foo', ('bar',), {}), + (('bar',), {'_query': {'y': 'foo'}})) + class DummyContext: pass -- cgit v1.2.3 From 737016eb553701ec154e33d212379a2356917e4c Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Wed, 16 Jul 2014 17:40:46 -0400 Subject: Handle list of tuples as query string. --- pyramid/static.py | 6 +++++- pyramid/tests/test_static.py | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pyramid/static.py b/pyramid/static.py index 9d691ca46..4ae00b056 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -206,7 +206,11 @@ class QueryStringCacheBuster(object): self.token = token def pregenerate(self, token, subpath, kw): - kw.setdefault('_query', {})[self.param] = token + query = kw.setdefault('_query', {}) + if isinstance(query, dict): + query[self.param] = token + else: + kw['_query'] = query + [(self.param, token)] return subpath, kw diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 5edb70b50..f7b580df2 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -453,6 +453,12 @@ class TestQueryStringCacheBuster(unittest.TestCase): fut('foo', ('bar',), {}), (('bar',), {'_query': {'y': 'foo'}})) + def test_pregenerate_query_is_already_tuples(self): + fut = self._makeOne().pregenerate + self.assertEqual( + fut('foo', ('bar',), {'_query': [('a', 'b')]}), + (('bar',), {'_query': [('a', 'b'), ('x', 'foo')]})) + class DummyContext: pass -- cgit v1.2.3 From d4da82c5ea9713f20205f86c3521db7ebabe2479 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Thu, 17 Jul 2014 09:26:28 -0400 Subject: Fix infinite loop in PY3. --- pyramid/static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/static.py b/pyramid/static.py index 4ae00b056..09743ac15 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -161,7 +161,7 @@ def _generate_md5(spec): asset = AssetResolver(None).resolve(spec) md5 = hashlib.md5() with asset.stream() as stream: - for block in iter(lambda: stream.read(4096), ''): + for block in iter(lambda: stream.read(4096), b''): md5.update(block) return md5.hexdigest() -- cgit v1.2.3 From f729a1e7f1efc27a6df1ae0eaca7fdffdd86ec2f Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Thu, 17 Jul 2014 16:04:28 -0400 Subject: Write the documentation. --- docs/api/interfaces.rst | 2 ++ docs/api/static.rst | 7 +++++ docs/narr/assets.rst | 72 ++++++++++++++++++++++++++++++++++++++++--------- pyramid/config/views.py | 15 +++++++---- pyramid/interfaces.py | 14 +++++++--- pyramid/static.py | 41 ++++++++++++++++++++++++++++ 6 files changed, 129 insertions(+), 22 deletions(-) diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index d8d935afd..a62976d8a 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -86,3 +86,5 @@ Other Interfaces .. autointerface:: IResourceURL :members: + .. autointerface:: ICacheBuster + :members: diff --git a/docs/api/static.rst b/docs/api/static.rst index c28473584..8ea2fff75 100644 --- a/docs/api/static.rst +++ b/docs/api/static.rst @@ -9,3 +9,10 @@ :members: :inherited-members: + .. autoclass:: PathSegmentCacheBuster + :members: + + .. autoclass:: QueryStringCacheBuster + :members: + + .. autofunction:: Md5AssetTokenGenerator diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index a2976de22..97d473761 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -315,7 +315,7 @@ requests a copy, regardless of any caching policy set for the resource's old URL. :app:`Pyramid` can be configured to produce cache busting URLs for static -assets by passing the optional argument, `cache_bust` to +assets by passing the optional argument, ``cachebuster`` to :meth:`~pyramid.config.Configurator.add_static_view`: .. code-block:: python @@ -323,27 +323,22 @@ assets by passing the optional argument, `cache_bust` to # config is an instance of pyramid.config.Configurator config.add_static_view(name='static', path='mypackage:folder/static', - cache_bust='md5') + cachebuster=True) -Supplying the `cache_bust` argument instructs :app:`Pyramid` to add a query -string to URLs generated for this static view which includes the md5 checksum -of the static file being served: +Setting the ``cachebuster`` argument instructs :app:`Pyramid` to use a cache +busting scheme which adds the md5 checksum for a static asset as a path segment +in the asset's URL: .. code-block:: python :linenos: js_url = request.static_url('mypackage:folder/static/js/myapp.js') - # Returns: 'http://www.example.com/static/js/myapp.js?md5=c9658b3c0a314a1ca21e5988e662a09e` + # Returns: 'http://www.example.com/static/c9658b3c0a314a1ca21e5988e662a09e/js/myapp.js` When the asset changes, so will its md5 checksum, and therefore so will its -URL. Supplying the `cache_bust` argument also causes the static view to set +URL. Supplying the ``cachebuster`` argument also causes the static view to set headers instructing clients to cache the asset for ten years, unless the -`max_cache_age` argument is also passed, in which case that value is used. - -.. note:: - - `md5` is currently the only possible value for the `cache_bust` argument to - :meth:`~pyramid.config.Configurator.add_static_view`. +``max_cache_age`` argument is also passed, in which case that value is used. .. note:: @@ -351,6 +346,57 @@ headers instructing clients to cache the asset for ten years, unless the restarting your application, you may still generate URLs with a stale md5 checksum. +Customizing the Cache Buster +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Revisiting from the previous section: + +.. code-block:: python + :linenos: + + # config is an instance of pyramid.config.Configurator + config.add_static_view(name='static', path='mypackage:folder/static', + cachebuster=True) + +Setting ``cachebuster`` to ``True`` instructs :app:`Pyramid` to use a default +cache busting implementation that should work for many situations. The +``cachebuster`` may be set to any object that implements the interface, +:class:`~pyramid.interfaces.ICacheBuster`. The above configuration is exactly +equivalent to: + +.. code-block:: python + :linenos: + + from pyramid.static import ( + Md5AssetTokenGenerator, + PathSegmentCacheBuster) + + # config is an instance of pyramid.config.Configurator + cachebuster = PathSegmentCacheBuster(Md5AssetTokenGenerator()) + config.add_static_view(name='static', path='mypackage:folder/static', + cachebuster=cachebuster) + +:app:`Pyramid` includes two ready to use cache buster implementations: +:class:`~pyramid.static.PathSegmentCacheBuster`, which inserts an asset token +in the path portion of the asset's URL, and +:class:`~pyramid.static.QueryStringCacheBuster`, which adds an asset token to +the query string of the asset's URL. Both of these classes have constructors +which accept a token generator function as an argument, allowing for the way a +token is generated to be decoupled from the way it is inserted into a URL. +:app:`Pyramid` provides a single asset token generator, +:meth:`~pyramid.static.Md5AssetTokenGenerator`. + +In order to implement your own cache buster, see the +:class:`~pyramid.interfaces.ICacheBuster` interface and the existing +implementations in the :mod:`~pyramid.static` module. + +.. note:: + + Many HTTP caching proxy implementations will fail to cache any URL which + has a query string. For this reason, you should probably prefer + :class:`~pyramid.static.PathSegementCacheBuster` to + :class:`~pyramid.static.QueryStringCacheBuster`. + .. index:: single: static assets view diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 00c5622e7..d74ecfadb 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1788,11 +1788,16 @@ class ViewsConfiguratorMixin(object): particular Expires or Cache-Control headers are set in the response, unless ``cache_bust`` is specified. - The ``cache_bust`` keyword argument may be set to ``"md5"`` to cause - :meth:`~pyramid.request.Request.static_url` to generate URLs with an - additional query string which includes the md5 checksum for the static - asset. This argument modifies the default for ``cache_max_age``, - making it ten years. ``cache_max_age`` may still be explicitly + The ``cachebuster`` keyword argument may be set to cause + :meth:`~pyramid.request.Request.static_url` to use cache busting when + generating URLs. See :ref:`cache_busting` for general information + about cache busting. The value of the ``cachebuster`` argument may be + ``True``, in which case a default cache busting implementation is used. + The value of the ``cachebuster`` argument may also be an object which + implements :class:`~pyramid.interfaces.ICacheBuster`. See the + :mod:`~pyramid.static` module for some implementations. If the + ``cachebuster`` argument is provided, the default for ``cache_max_age`` + is modified to be ten years. ``cache_max_age`` may still be explicitly provided to override this default. The ``permission`` keyword argument is used to specify the diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 822d1624c..f3d7b1798 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1168,6 +1168,12 @@ class ICacheBuster(Interface): """ A container for functions which implement a cache busting policy for serving static assets. + + The implementations provided by :app:`Pyramid` use standard instance + methods for ``pregenerate`` and ``match``, while accepting an + implementation of ``token`` as an argument to their constructor. This + pattern allows for the decoupling of how a token is generated and how it is + inserted into a URL. For examples see the :mod:`~pyramid.static` module. """ def token(pathspec): """ @@ -1197,7 +1203,7 @@ class ICacheBuster(Interface): A function which modifies a subpath and/or keyword arguments from which a static asset URL will be computed during URL generation. The ``token`` argument is a token string computed by an instance of - :method:`~pyramid.interfaces.ICacheBuster.token` for a particular + :meth:`~pyramid.interfaces.ICacheBuster.token` for a particular asset. The ``subpath`` argument is a tuple of path elements that represent the portion of the asset URL which is used to find the asset. The ``kw`` argument is a dict of keywords that are to be passed @@ -1218,9 +1224,9 @@ class ICacheBuster(Interface): def match(subpath): """ - A function which performs the logical inverse of an - :method:`~pyramid.interfaces.ICacheBuster.pregenerate`, by taking a - subpath from a cache busted URL and removing the cachebust token, so + A function which performs the logical inverse of + :meth:`~pyramid.interfaces.ICacheBuster.pregenerate` by taking a + subpath from a cache busted URL and removing the cache bust token, so that :app:`Pyramid` can find the underlying asset. ``subpath`` is the subpath portion of the URL for an incoming request diff --git a/pyramid/static.py b/pyramid/static.py index 09743ac15..ab9d47aa5 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -166,6 +166,12 @@ def _generate_md5(spec): return md5.hexdigest() def Md5AssetTokenGenerator(): + """ + A factory method which returns a function that implements + :meth:`~pyramid.interfaces.ICacheBuster.token`. The function computes and + returns md5 checksums for static assets, caching them in memory for speedy + retrieval on subsequent calls. + """ token_cache = {} def generate_token(pathspec): @@ -189,7 +195,23 @@ def Md5AssetTokenGenerator(): return generate_token class PathSegmentCacheBuster(object): + """ + An implementation of :class:`~pyramid.interfaces.ICacheBuster` which + inserts a token for cache busting in the path portion of an asset URL. + + The ``token`` argument should be an implementation of + :meth:`~pyramid.interfaces.ICacheBuster.token`. For example, to use + this cache buster with an md5 token generator: + .. code-block:: python + :linenos: + + from pyramid.static import ( + Md5AssetTokenGenerator, + PathSegmentCacheBuster) + + cachebuster = PathSegmentCacheBuster(Md5AssetTokenGenerator()) + """ def __init__(self, token): self.token = token @@ -200,7 +222,26 @@ class PathSegmentCacheBuster(object): return subpath[1:] class QueryStringCacheBuster(object): + """ + An implementation of :class:`~pyramid.interfaces.ICacheBuster` which + adds a token for cache busting in the query string of an asset URL. + + The ``token`` argument should be an implementation of + :meth:`~pyramid.interfaces.ICacheBuster.token`. For example, to use + this cache buster with an md5 token generator: + + .. code-block:: python + :linenos: + from pyramid.static import ( + Md5AssetTokenGenerator, + PathSegmentCacheBuster) + + cachebuster = QueryStringCacheBuster(Md5AssetTokenGenerator()) + + The optional ``param`` argument determines the name of the parameter added + to the query string and defaults to ``'x'``. + """ def __init__(self, token, param='x'): self.param = param self.token = token -- cgit v1.2.3 From 002da7991f4433e5fd5a07489038a6bd2720a526 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Thu, 17 Jul 2014 16:10:52 -0400 Subject: Add index entry. --- docs/narr/assets.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 97d473761..642211f5b 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -286,6 +286,9 @@ the application is being run in development or in production (use a different suggestion for a pattern; any setting name other than ``media_location`` could be used. +.. index:: + single: Cache Busting + .. _cache_busting: Cache Busting -- cgit v1.2.3 From aa96dda157d39c57c0d2fe8399db0b2175fa83d2 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Fri, 18 Jul 2014 17:18:56 -0400 Subject: Take mcdonc's advice. This should be easier for users to understand. --- docs/api/static.rst | 2 - docs/narr/assets.rst | 41 +++++++++++++------- pyramid/config/views.py | 8 +--- pyramid/static.py | 66 ++++++++++----------------------- pyramid/tests/test_config/test_views.py | 20 +++++----- pyramid/tests/test_static.py | 46 ++++++++++++++--------- 6 files changed, 87 insertions(+), 96 deletions(-) diff --git a/docs/api/static.rst b/docs/api/static.rst index 8ea2fff75..de5bcabda 100644 --- a/docs/api/static.rst +++ b/docs/api/static.rst @@ -14,5 +14,3 @@ .. autoclass:: QueryStringCacheBuster :members: - - .. autofunction:: Md5AssetTokenGenerator diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 642211f5b..7987d03a6 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -370,28 +370,18 @@ equivalent to: .. code-block:: python :linenos: - from pyramid.static import ( - Md5AssetTokenGenerator, - PathSegmentCacheBuster) + from pyramid.static import PathSegmentCacheBuster # config is an instance of pyramid.config.Configurator - cachebuster = PathSegmentCacheBuster(Md5AssetTokenGenerator()) config.add_static_view(name='static', path='mypackage:folder/static', - cachebuster=cachebuster) + cachebuster=PathSegmentCacheBuster()) :app:`Pyramid` includes two ready to use cache buster implementations: :class:`~pyramid.static.PathSegmentCacheBuster`, which inserts an asset token in the path portion of the asset's URL, and :class:`~pyramid.static.QueryStringCacheBuster`, which adds an asset token to -the query string of the asset's URL. Both of these classes have constructors -which accept a token generator function as an argument, allowing for the way a -token is generated to be decoupled from the way it is inserted into a URL. -:app:`Pyramid` provides a single asset token generator, -:meth:`~pyramid.static.Md5AssetTokenGenerator`. - -In order to implement your own cache buster, see the -:class:`~pyramid.interfaces.ICacheBuster` interface and the existing -implementations in the :mod:`~pyramid.static` module. +the query string of the asset's URL. Both of these classes generate md5 +checksums as asset tokens. .. note:: @@ -400,6 +390,29 @@ implementations in the :mod:`~pyramid.static` module. :class:`~pyramid.static.PathSegementCacheBuster` to :class:`~pyramid.static.QueryStringCacheBuster`. +In order to implement your own cache buster, you can write your own class from +scratch which implements the :class:`~pyramid.interfaces.ICacheBuster` +interface. Alternatively you may choose to subclass one of the existing +implementations. One of the most likely scenarios is you'd want to change the +way the asset token is generated. To do this just subclass an existing +implementation and replace the :meth:`~pyramid.interfaces.ICacheBuster.token` +method. Here is an example which just uses a global setting for the asset +token: + +.. code-block:: python + :linenos: + + from pyramid.static import PathSegmentCacheBuster + + class MyCacheBuster(PathSegmentCacheBuster): + + def __init__(self, config): + # config is an instance of pyramid.config.Configurator + self._token = config.registry.settings['myapp.cachebust_token'] + + def token(self, pathspec): + return self._token + .. index:: single: static assets view diff --git a/pyramid/config/views.py b/pyramid/config/views.py index d74ecfadb..f186a44ae 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1897,11 +1897,7 @@ def isexception(o): @implementer(IStaticURLInfo) class StaticURLInfo(object): # Indirection for testing - _default_cachebuster = staticmethod(PathSegmentCacheBuster) - _default_asset_token_generator = staticmethod(Md5AssetTokenGenerator) - - def _make_default_cachebuster(self): - return self._default_cachebuster(self._default_asset_token_generator()) + _default_cachebuster = PathSegmentCacheBuster def _get_registrations(self, registry): try: @@ -1964,7 +1960,7 @@ class StaticURLInfo(object): cb = extra.pop('cachebuster', None) if cb is True: - cb = self._make_default_cachebuster() + cb = self._default_cachebuster() if cb: def cachebuster(subpath, kw): token = cb.token(spec + subpath) diff --git a/pyramid/static.py b/pyramid/static.py index ab9d47aa5..34fc3f55c 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -165,16 +165,16 @@ def _generate_md5(spec): md5.update(block) return md5.hexdigest() -def Md5AssetTokenGenerator(): +class Md5AssetTokenGenerator(object): """ - A factory method which returns a function that implements - :meth:`~pyramid.interfaces.ICacheBuster.token`. The function computes and - returns md5 checksums for static assets, caching them in memory for speedy - retrieval on subsequent calls. + A mixin class which provides an implementation of + :meth:`~pyramid.interfaces.ICacheBuster.target` which generates an md5 + checksum token for an asset, caching it for subsequent calls. """ - token_cache = {} + def __init__(self): + self.token_cache = {} - def generate_token(pathspec): + def token(self, pathspec): # An astute observer will notice that this use of token_cache doesn't # look particularly thread safe. Basic read/write operations on Python # dicts, however, are atomic, so simply accessing and writing values @@ -187,64 +187,36 @@ def Md5AssetTokenGenerator(): # the extra overhead of using locks to serialize access to the dict # seems an unnecessary burden. # - token = token_cache.get(pathspec) + token = self.token_cache.get(pathspec) if not token: - token_cache[pathspec] = token = _generate_md5(pathspec) + self.token_cache[pathspec] = token = _generate_md5(pathspec) return token - return generate_token - -class PathSegmentCacheBuster(object): +class PathSegmentCacheBuster(Md5AssetTokenGenerator): """ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which - inserts a token for cache busting in the path portion of an asset URL. - - The ``token`` argument should be an implementation of - :meth:`~pyramid.interfaces.ICacheBuster.token`. For example, to use - this cache buster with an md5 token generator: - - .. code-block:: python - :linenos: - - from pyramid.static import ( - Md5AssetTokenGenerator, - PathSegmentCacheBuster) - - cachebuster = PathSegmentCacheBuster(Md5AssetTokenGenerator()) + inserts an md5 checksum token for cache busting in the path portion of an + asset URL. Generated md5 checksums are cached in order to speed up + subsequent calls. """ - def __init__(self, token): - self.token = token - def pregenerate(self, token, subpath, kw): return (token,) + subpath, kw def match(self, subpath): return subpath[1:] -class QueryStringCacheBuster(object): +class QueryStringCacheBuster(Md5AssetTokenGenerator): """ - An implementation of :class:`~pyramid.interfaces.ICacheBuster` which - adds a token for cache busting in the query string of an asset URL. - - The ``token`` argument should be an implementation of - :meth:`~pyramid.interfaces.ICacheBuster.token`. For example, to use - this cache buster with an md5 token generator: - - .. code-block:: python - :linenos: - - from pyramid.static import ( - Md5AssetTokenGenerator, - PathSegmentCacheBuster) - - cachebuster = QueryStringCacheBuster(Md5AssetTokenGenerator()) + An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds a + token for cache busting in the query string of an asset URL. Generated md5 + checksums are cached in order to speed up subsequent calls. The optional ``param`` argument determines the name of the parameter added to the query string and defaults to ``'x'``. """ - def __init__(self, token, param='x'): + def __init__(self, param='x'): + super(QueryStringCacheBuster, self).__init__() self.param = param - self.token = token def pregenerate(self, token, subpath, kw): query = kw.setdefault('_query', {}) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 0b81f5a6f..10a2f6f53 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3944,19 +3944,14 @@ class TestStaticURLInfo(unittest.TestCase): def test_add_cachebust_default(self): config = self._makeConfig() inst = self._makeOne() - inst._default_asset_token_generator = lambda: lambda pathspec: 'foo' + inst._default_cachebuster = DummyCacheBuster inst.add(config, 'view', 'mypackage:path', cachebuster=True) cachebuster = config.registry._static_url_registrations[0][3] - subpath, _ = cachebuster('some/path', None) - self.assertEqual(subpath, 'foo/some/path') + subpath, kw = cachebuster('some/path', {}) + self.assertEqual(subpath, 'some/path') + self.assertEqual(kw['x'], 'foo') def test_add_cachebust_custom(self): - class DummyCacheBuster(object): - def token(self, pathspec): - return 'foo' - def pregenerate(self, token, subpath, kw): - kw['x'] = token - return subpath, kw config = self._makeConfig() inst = self._makeOne() inst.add(config, 'view', 'mypackage:path', @@ -4071,6 +4066,13 @@ class DummyMultiView: def __permitted__(self, context, request): """ """ +class DummyCacheBuster(object): + def token(self, pathspec): + return 'foo' + def pregenerate(self, token, subpath, kw): + kw['x'] = token + return subpath, kw + def parse_httpdate(s): import datetime # cannot use %Z, must use literal GMT; Jython honors timezone diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index f7b580df2..6ae9b13db 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -369,29 +369,33 @@ class Test_static_view_use_subpath_True(unittest.TestCase): self.assertRaises(HTTPNotFound, inst, context, request) class TestMd5AssetTokenGenerator(unittest.TestCase): + _fspath = None + + @property + def fspath(self): + if self._fspath: + return self._fspath - def setUp(self): import os import tempfile - self.tmp = tempfile.mkdtemp() - self.fspath = os.path.join(self.tmp, 'test.txt') - - def tearDown(self): import shutil - shutil.rmtree(self.tmp) + tmp = tempfile.mkdtemp() + self.addCleanup(lambda: shutil.rmtree(tmp)) + self._fspath = os.path.join(tmp, 'test.txt') + return self._fspath def _makeOne(self): - from pyramid.static import Md5AssetTokenGenerator as unit - return unit() + from pyramid.static import Md5AssetTokenGenerator as cls + return cls() def test_package_resource(self): - fut = self._makeOne() + fut = self._makeOne().token expected = '76d653a3a044e2f4b38bb001d283e3d9' token = fut('pyramid.tests:fixtures/static/index.html') self.assertEqual(token, expected) def test_filesystem_resource(self): - fut = self._makeOne() + fut = self._makeOne().token expected = 'd5155f250bef0e9923e894dbc713c5dd' with open(self.fspath, 'w') as f: f.write("Are we rich yet?") @@ -399,7 +403,7 @@ class TestMd5AssetTokenGenerator(unittest.TestCase): self.assertEqual(token, expected) def test_cache(self): - fut = self._makeOne() + fut = self._makeOne().token expected = 'd5155f250bef0e9923e894dbc713c5dd' with open(self.fspath, 'w') as f: f.write("Are we rich yet?") @@ -415,8 +419,10 @@ class TestMd5AssetTokenGenerator(unittest.TestCase): class TestPathSegmentCacheBuster(unittest.TestCase): def _makeOne(self): - from pyramid.static import PathSegmentCacheBuster as unit - return unit(lambda pathspec: 'foo') + from pyramid.static import PathSegmentCacheBuster as cls + inst = cls() + inst.token = lambda pathspec: 'foo' + return inst def test_token(self): fut = self._makeOne().token @@ -432,9 +438,14 @@ class TestPathSegmentCacheBuster(unittest.TestCase): class TestQueryStringCacheBuster(unittest.TestCase): - def _makeOne(self): - from pyramid.static import QueryStringCacheBuster as unit - return unit(lambda pathspec: 'foo') + def _makeOne(self, param=None): + from pyramid.static import QueryStringCacheBuster as cls + if param: + inst = cls(param) + else: + inst = cls() + inst.token = lambda pathspec: 'foo' + return inst def test_token(self): fut = self._makeOne().token @@ -447,8 +458,7 @@ class TestQueryStringCacheBuster(unittest.TestCase): (('bar',), {'_query': {'x': 'foo'}})) def test_pregenerate_change_param(self): - from pyramid.static import QueryStringCacheBuster as unit - fut = unit(lambda pathspec: 'foo', 'y').pregenerate + fut = self._makeOne('y').pregenerate self.assertEqual( fut('foo', ('bar',), {}), (('bar',), {'_query': {'y': 'foo'}})) -- cgit v1.2.3 From 6596304446f8369dfbcf264d143fe85d75832dba Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Mon, 21 Jul 2014 16:46:35 -0400 Subject: Add 'prevent_cachebuster' setting. --- docs/narr/assets.rst | 11 ++++++++++- docs/narr/environment.rst | 20 +++++++++++++++++++ pyramid/config/settings.py | 9 ++++++++- pyramid/config/views.py | 12 ++++++------ pyramid/tests/test_config/test_settings.py | 31 +++++++++++++++++++++++++++++- pyramid/tests/test_config/test_views.py | 11 ++++++++++- 6 files changed, 84 insertions(+), 10 deletions(-) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 7987d03a6..fea3fae48 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -349,6 +349,15 @@ headers instructing clients to cache the asset for ten years, unless the restarting your application, you may still generate URLs with a stale md5 checksum. +Disabling the Cache Buster +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It can be useful in some situations (e.g. development) to globally disable all +configured cache busters without changing calls to +:meth:`~pyramid.config.Configurator.add_static_view`. To do this set the +``PYRAMID_PREVENT_CACHEBUSTER`` environment variable or the +``pyramid.prevent_cachebuster`` configuration value to a true value. + Customizing the Cache Buster ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -387,7 +396,7 @@ checksums as asset tokens. Many HTTP caching proxy implementations will fail to cache any URL which has a query string. For this reason, you should probably prefer - :class:`~pyramid.static.PathSegementCacheBuster` to + :class:`~pyramid.static.PathSegmentCacheBuster` to :class:`~pyramid.static.QueryStringCacheBuster`. In order to implement your own cache buster, you can write your own class from diff --git a/docs/narr/environment.rst b/docs/narr/environment.rst index 412635f08..7e2f19278 100644 --- a/docs/narr/environment.rst +++ b/docs/narr/environment.rst @@ -158,6 +158,26 @@ feature when this is true. | | | +---------------------------------+----------------------------------+ +Preventing Cache Busting +------------------------ + +Prevent the ``cachebuster`` static view configuration argument from having any +effect globally in this process when this value is true. No cache buster will +be configured or used when this is true. + +.. seealso:: + + See also :ref:`cache_busting`. + ++---------------------------------+----------------------------------+ +| Environment Variable Name | Config File Setting Name | ++=================================+==================================+ +| ``PYRAMID_PREVENT_CACHEBUSTER`` | ``pyramid.prevent_cachebuster`` | +| | or ``prevent_cachebuster`` | +| | | +| | | ++---------------------------------+----------------------------------+ + Debugging All ------------- diff --git a/pyramid/config/settings.py b/pyramid/config/settings.py index 565a6699c..4d7af6015 100644 --- a/pyramid/config/settings.py +++ b/pyramid/config/settings.py @@ -17,7 +17,7 @@ class SettingsConfiguratorMixin(object): def add_settings(self, settings=None, **kw): """Augment the :term:`deployment settings` with one or more - key/value pairs. + key/value pairs. You may pass a dictionary:: @@ -117,6 +117,11 @@ class Settings(dict): config_prevent_http_cache) eff_prevent_http_cache = asbool(eget('PYRAMID_PREVENT_HTTP_CACHE', config_prevent_http_cache)) + config_prevent_cachebuster = self.get('prevent_cachebuster', '') + config_prevent_cachebuster = self.get('pyramid.prevent_cachebuster', + config_prevent_cachebuster) + eff_prevent_cachebuster = asbool(eget('PYRAMID_PREVENT_CACHEBUSTER', + config_prevent_cachebuster)) update = { 'debug_authorization': eff_debug_all or eff_debug_auth, @@ -128,6 +133,7 @@ class Settings(dict): 'reload_assets':eff_reload_all or eff_reload_assets, 'default_locale_name':eff_locale_name, 'prevent_http_cache':eff_prevent_http_cache, + 'prevent_cachebuster':eff_prevent_cachebuster, 'pyramid.debug_authorization': eff_debug_all or eff_debug_auth, 'pyramid.debug_notfound': eff_debug_all or eff_debug_notfound, @@ -138,6 +144,7 @@ class Settings(dict): 'pyramid.reload_assets':eff_reload_all or eff_reload_assets, 'pyramid.default_locale_name':eff_locale_name, 'pyramid.prevent_http_cache':eff_prevent_http_cache, + 'pyramid.prevent_cachebuster':eff_prevent_cachebuster, } self.update(update) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index f186a44ae..62feca77e 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -34,10 +34,7 @@ from pyramid.interfaces import ( ) from pyramid import renderers -from pyramid.static import ( - Md5AssetTokenGenerator, - PathSegmentCacheBuster, -) +from pyramid.static import PathSegmentCacheBuster from pyramid.compat import ( string_types, @@ -1786,7 +1783,7 @@ class ViewsConfiguratorMixin(object): Note that this argument has no effect when the ``name`` is a *url prefix*. By default, this argument is ``None``, meaning that no particular Expires or Cache-Control headers are set in the response, - unless ``cache_bust`` is specified. + unless ``cachebuster`` is specified. The ``cachebuster`` keyword argument may be set to cause :meth:`~pyramid.request.Request.static_url` to use cache busting when @@ -1958,7 +1955,10 @@ class StaticURLInfo(object): # make sure it ends with a slash name = name + '/' - cb = extra.pop('cachebuster', None) + if config.registry.settings.get('pyramid.prevent_cachebuster'): + cb = None + else: + cb = extra.pop('cachebuster', None) if cb is True: cb = self._default_cachebuster() if cb: diff --git a/pyramid/tests/test_config/test_settings.py b/pyramid/tests/test_config/test_settings.py index c74f96375..7cf550c1d 100644 --- a/pyramid/tests/test_config/test_settings.py +++ b/pyramid/tests/test_config/test_settings.py @@ -57,7 +57,7 @@ class TestSettingsConfiguratorMixin(unittest.TestCase): self.assertEqual(settings['a'], 1) class TestSettings(unittest.TestCase): - + def _getTargetClass(self): from pyramid.config.settings import Settings return Settings @@ -131,6 +131,35 @@ class TestSettings(unittest.TestCase): self.assertEqual(result['prevent_http_cache'], True) self.assertEqual(result['pyramid.prevent_http_cache'], True) + def test_prevent_cachebuster(self): + settings = self._makeOne({}) + self.assertEqual(settings['prevent_cachebuster'], False) + self.assertEqual(settings['pyramid.prevent_cachebuster'], False) + result = self._makeOne({'prevent_cachebuster':'false'}) + self.assertEqual(result['prevent_cachebuster'], False) + self.assertEqual(result['pyramid.prevent_cachebuster'], False) + result = self._makeOne({'prevent_cachebuster':'t'}) + self.assertEqual(result['prevent_cachebuster'], True) + self.assertEqual(result['pyramid.prevent_cachebuster'], True) + result = self._makeOne({'prevent_cachebuster':'1'}) + self.assertEqual(result['prevent_cachebuster'], True) + self.assertEqual(result['pyramid.prevent_cachebuster'], True) + result = self._makeOne({'pyramid.prevent_cachebuster':'t'}) + self.assertEqual(result['prevent_cachebuster'], True) + self.assertEqual(result['pyramid.prevent_cachebuster'], True) + result = self._makeOne({}, {'PYRAMID_PREVENT_CACHEBUSTER':'1'}) + self.assertEqual(result['prevent_cachebuster'], True) + self.assertEqual(result['pyramid.prevent_cachebuster'], True) + result = self._makeOne({'prevent_cachebuster':'false', + 'pyramid.prevent_cachebuster':'1'}) + self.assertEqual(result['prevent_cachebuster'], True) + self.assertEqual(result['pyramid.prevent_cachebuster'], True) + result = self._makeOne({'prevent_cachebuster':'false', + 'pyramid.prevent_cachebuster':'f'}, + {'PYRAMID_PREVENT_CACHEBUSTER':'1'}) + self.assertEqual(result['prevent_cachebuster'], True) + self.assertEqual(result['pyramid.prevent_cachebuster'], True) + def test_reload_templates(self): settings = self._makeOne({}) self.assertEqual(settings['reload_templates'], False) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 10a2f6f53..8f600c1d2 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3951,6 +3951,14 @@ class TestStaticURLInfo(unittest.TestCase): self.assertEqual(subpath, 'some/path') self.assertEqual(kw['x'], 'foo') + def test_add_cachebust_prevented(self): + config = self._makeConfig() + config.registry.settings['pyramid.prevent_cachebuster'] = True + inst = self._makeOne() + inst.add(config, 'view', 'mypackage:path', cachebuster=True) + cachebuster = config.registry._static_url_registrations[0][3] + self.assertEqual(cachebuster, None) + def test_add_cachebust_custom(self): config = self._makeConfig() inst = self._makeOne() @@ -3980,7 +3988,8 @@ class Test_view_description(unittest.TestCase): class DummyRegistry: - pass + def __init__(self): + self.settings = {} from zope.interface import implementer from pyramid.interfaces import IResponse -- cgit v1.2.3 From 026e292aa8d6da7a2e62eab05d8ceb5f061ac44e Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Mon, 21 Jul 2014 17:26:17 -0400 Subject: Fix tests on py26. --- pyramid/tests/test_static.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 6ae9b13db..aca5c4bbd 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -370,6 +370,7 @@ class Test_static_view_use_subpath_True(unittest.TestCase): class TestMd5AssetTokenGenerator(unittest.TestCase): _fspath = None + _tmp = None @property def fspath(self): @@ -378,12 +379,15 @@ class TestMd5AssetTokenGenerator(unittest.TestCase): import os import tempfile - import shutil - tmp = tempfile.mkdtemp() - self.addCleanup(lambda: shutil.rmtree(tmp)) + self._tmp = tmp = tempfile.mkdtemp() self._fspath = os.path.join(tmp, 'test.txt') return self._fspath + def tearDown(self): + import shutil + if self._tmp: + shutil.rmtree(self._tmp) + def _makeOne(self): from pyramid.static import Md5AssetTokenGenerator as cls return cls() -- cgit v1.2.3 From 9177d010e6646161e674b82b88bf177ebe8a7b2e Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Mon, 21 Jul 2014 17:29:11 -0400 Subject: Update change log. --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 51af8ee01..54c1a20ed 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,12 @@ Next release ============ +Features +-------- + +- Cache busting for static resources has been added and is available via a new + argument to ``pyramid.config.Configurator.add_static_view``: ``cachebuster``. + Bug Fixes --------- -- cgit v1.2.3 From 168a31d375e35b93c4330c1fd296b1c4ff641029 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 22 Jul 2014 01:08:04 -0700 Subject: fix URL Schema does not change when only host is overridden. --- pyramid/url.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/url.py b/pyramid/url.py index bf4d4ff48..a0f3d7f2f 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -223,7 +223,7 @@ class URLMethodsMixin(object): named portion in the generated URL. For example, if you pass ``_host='foo.com'``, and the URL that would have been generated without the host replacement is ``http://example.com/a``, the result - will be ``https://foo.com/a``. + will be ``http://foo.com/a``. Note that if ``_scheme`` is passed as ``https``, and ``_port`` is not passed, the ``_port`` value is assumed to have been passed as @@ -414,7 +414,7 @@ class URLMethodsMixin(object): portion in the generated URL. For example, if you pass ``host='foo.com'``, and the URL that would have been generated without the host replacement is ``http://example.com/a``, the result - will be ``https://foo.com/a``. + will be ``http://foo.com/a``. If ``scheme`` is passed as ``https``, and an explicit ``port`` is not passed, the ``port`` value is assumed to have been passed as ``443``. -- cgit v1.2.3 From 40c6bfa85a75ffacf23a3ccd128dc5b8bb57a464 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Tue, 22 Jul 2014 09:28:17 -0400 Subject: Forgot to update interface docs earlier. --- pyramid/interfaces.py | 74 ++++++++++++++++----------------------------------- 1 file changed, 23 insertions(+), 51 deletions(-) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index f3d7b1798..2b6ba7eb6 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1166,65 +1166,37 @@ class IPredicateList(Interface): class ICacheBuster(Interface): """ - A container for functions which implement a cache busting policy for - serving static assets. - - The implementations provided by :app:`Pyramid` use standard instance - methods for ``pregenerate`` and ``match``, while accepting an - implementation of ``token`` as an argument to their constructor. This - pattern allows for the decoupling of how a token is generated and how it is - inserted into a URL. For examples see the :mod:`~pyramid.static` module. + Instances of ``ICacheBuster`` may be provided as arguments to + :meth:`~pyramid.config.Configurator.add_static_view`. Instances of + ``ICacheBuster`` provide mechanisms for generating a cache bust token for + a static asset, modifying a static asset URL to include a cache bust token, + and, optionally, unmodifying a static asset URL in order to look up an + asset. See :ref:`cache_busting`. """ def token(pathspec): """ - A function which computes and returns a token string used for cache - busting. ``pathspec`` is the path specification for the resource to be - cache busted. Often a cachebust token might be computed for a specific - asset (e.g. an md5 checksum), but probably just as often people use - schemes where a single cachebust token is used globally. It could be a - git commit sha1, a timestamp, or something configured manually. A - pattern that can be useful is to use to a factory function and a - closure to return a function that depends on some configuration. For - example: - - .. code-block:: python - :linenos: - - def use_configured_cachebust_token(config): - # config is an instance of pyramid.config.Configurator - token = config.registry.settings['myapp.cachebust_token'] - def cachebust_token(pathspec): - return token - return cachebust_token - """ + Computes and returns a token string used for cache busting. + ``pathspec`` is the path specification for the resource to be cache + busted. """ def pregenerate(token, subpath, kw): """ - A function which modifies a subpath and/or keyword arguments from which - a static asset URL will be computed during URL generation. The - ``token`` argument is a token string computed by an instance of - :meth:`~pyramid.interfaces.ICacheBuster.token` for a particular - asset. The ``subpath`` argument is a tuple of path elements that - represent the portion of the asset URL which is used to find the asset. - The ``kw`` argument is a dict of keywords that are to be passed - eventually to :meth:`~pyramid.request.Request.route_url` for URL - generation. The return value of this function should be two-tuple of - ``(subpath, kw)`` which are versions of the same arguments modified to - include the cachebust token in the generated URL. - - Here is an example which places the token in a query string: - - .. code-block:: python - :linenos: - - def cb_pregen(token, subpath kw): - kw.setdefault('_query', {})['cb'] = token - return subpath, kw + Modifies a subpath and/or keyword arguments from which a static asset + URL will be computed during URL generation. The ``token`` argument is + a token string computed by + :meth:`~pyramid.interfaces.ICacheBuster.token` for a particular asset. + The ``subpath`` argument is a tuple of path elements that represent the + portion of the asset URL which is used to find the asset. The ``kw`` + argument is a dict of keywords that are to be passed eventually to + :meth:`~pyramid.request.Request.route_url` for URL generation. The + return value should be a two-tuple of ``(subpath, kw)`` which are + versions of the same arguments modified to include the cachebust token + in the generated URL. """ def match(subpath): """ - A function which performs the logical inverse of + Performs the logical inverse of :meth:`~pyramid.interfaces.ICacheBuster.pregenerate` by taking a subpath from a cache busted URL and removing the cache bust token, so that :app:`Pyramid` can find the underlying asset. @@ -1234,10 +1206,10 @@ class ICacheBuster(Interface): cache busting token elided. If the cache busting scheme in use doesn't specifically modify the path - portion of the generated URL (e.g. it adds a query string), a function + portion of the generated URL (e.g. it adds a query string), a method which implements this interface may not be necessary. It is permissible for an instance of - :class:`~pyramid.interfaces.ICacheBuster` to omit this function. + :class:`~pyramid.interfaces.ICacheBuster` to omit this method. """ # configuration phases: a lower phase number means the actions associated -- cgit v1.2.3 From 4d32f73a86e7223dbdb96b39193d357b38ea1a13 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Tue, 22 Jul 2014 10:00:47 -0400 Subject: Make sure any sequence type works with _query. --- pyramid/static.py | 2 +- pyramid/tests/test_static.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pyramid/static.py b/pyramid/static.py index 34fc3f55c..0cbb5533f 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -223,7 +223,7 @@ class QueryStringCacheBuster(Md5AssetTokenGenerator): if isinstance(query, dict): query[self.param] = token else: - kw['_query'] = query + [(self.param, token)] + kw['_query'] = tuple(query) + ((self.param, token),) return subpath, kw diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index aca5c4bbd..134bea25e 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -471,7 +471,13 @@ class TestQueryStringCacheBuster(unittest.TestCase): fut = self._makeOne().pregenerate self.assertEqual( fut('foo', ('bar',), {'_query': [('a', 'b')]}), - (('bar',), {'_query': [('a', 'b'), ('x', 'foo')]})) + (('bar',), {'_query': (('a', 'b'), ('x', 'foo'))})) + + def test_pregenerate_query_is_tuple_of_tuples(self): + fut = self._makeOne().pregenerate + self.assertEqual( + fut('foo', ('bar',), {'_query': (('a', 'b'),)}), + (('bar',), {'_query': (('a', 'b'), ('x', 'foo'))})) class DummyContext: pass -- cgit v1.2.3 From 63e6e14a83a10331635bdced1ff6258e4c18989c Mon Sep 17 00:00:00 2001 From: Dobes Vandermeer Date: Wed, 23 Jul 2014 09:49:44 -0700 Subject: Add a test case for a response callback adding a response callback when it is the only callback. --- pyramid/tests/test_request.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index ed41b62ff..ec206dad3 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -144,6 +144,29 @@ class TestRequest(unittest.TestCase): self.assertEqual(response.called2, True) self.assertEqual(inst.response_callbacks, []) + def test__process_response_callback_adding_response_callback(self): + """ + When a response callback adds another callback, that new callback should still be called. + + See https://github.com/Pylons/pyramid/pull/1373 + """ + inst = self._makeOne() + def callback1(request, response): + request.called1 = True + response.called1 = True + request.add_response_callback(callback2) + def callback2(request, response): + request.called2 = True + response.called2 = True + inst.add_response_callback(callback1) + response = DummyResponse() + inst._process_response_callbacks(response) + self.assertEqual(inst.called1, True) + self.assertEqual(inst.called2, True) + self.assertEqual(response.called1, True) + self.assertEqual(response.called2, True) + self.assertEqual(inst.response_callbacks, []) + def test_add_finished_callback(self): inst = self._makeOne() self.assertEqual(inst.finished_callbacks, ()) -- cgit v1.2.3 From 15b979413c700fbc289328b25aaa4ba1c4cbdda9 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Thu, 24 Jul 2014 17:13:08 -0400 Subject: cachebuster -> cachebust --- CHANGES.txt | 2 +- docs/narr/assets.rst | 20 +++++------ docs/narr/environment.rst | 6 ++-- pyramid/config/settings.py | 14 ++++---- pyramid/config/views.py | 30 ++++++++--------- pyramid/tests/test_config/test_settings.py | 54 +++++++++++++++--------------- pyramid/tests/test_config/test_views.py | 28 ++++++++-------- 7 files changed, 77 insertions(+), 77 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 54c1a20ed..63987d980 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -5,7 +5,7 @@ Features -------- - Cache busting for static resources has been added and is available via a new - argument to ``pyramid.config.Configurator.add_static_view``: ``cachebuster``. + argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``. Bug Fixes --------- diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index fea3fae48..7fb0ec40b 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -318,7 +318,7 @@ requests a copy, regardless of any caching policy set for the resource's old URL. :app:`Pyramid` can be configured to produce cache busting URLs for static -assets by passing the optional argument, ``cachebuster`` to +assets by passing the optional argument, ``cachebust`` to :meth:`~pyramid.config.Configurator.add_static_view`: .. code-block:: python @@ -326,9 +326,9 @@ assets by passing the optional argument, ``cachebuster`` to # config is an instance of pyramid.config.Configurator config.add_static_view(name='static', path='mypackage:folder/static', - cachebuster=True) + cachebust=True) -Setting the ``cachebuster`` argument instructs :app:`Pyramid` to use a cache +Setting the ``cachebust`` argument instructs :app:`Pyramid` to use a cache busting scheme which adds the md5 checksum for a static asset as a path segment in the asset's URL: @@ -339,7 +339,7 @@ in the asset's URL: # Returns: 'http://www.example.com/static/c9658b3c0a314a1ca21e5988e662a09e/js/myapp.js` When the asset changes, so will its md5 checksum, and therefore so will its -URL. Supplying the ``cachebuster`` argument also causes the static view to set +URL. Supplying the ``cachebust`` argument also causes the static view to set headers instructing clients to cache the asset for ten years, unless the ``max_cache_age`` argument is also passed, in which case that value is used. @@ -355,8 +355,8 @@ Disabling the Cache Buster It can be useful in some situations (e.g. development) to globally disable all configured cache busters without changing calls to :meth:`~pyramid.config.Configurator.add_static_view`. To do this set the -``PYRAMID_PREVENT_CACHEBUSTER`` environment variable or the -``pyramid.prevent_cachebuster`` configuration value to a true value. +``PYRAMID_PREVENT_CACHEBUST`` environment variable or the +``pyramid.prevent_cachebust`` configuration value to a true value. Customizing the Cache Buster ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -368,11 +368,11 @@ Revisiting from the previous section: # config is an instance of pyramid.config.Configurator config.add_static_view(name='static', path='mypackage:folder/static', - cachebuster=True) + cachebust=True) -Setting ``cachebuster`` to ``True`` instructs :app:`Pyramid` to use a default +Setting ``cachebust`` to ``True`` instructs :app:`Pyramid` to use a default cache busting implementation that should work for many situations. The -``cachebuster`` may be set to any object that implements the interface, +``cachebust`` may be set to any object that implements the interface, :class:`~pyramid.interfaces.ICacheBuster`. The above configuration is exactly equivalent to: @@ -383,7 +383,7 @@ equivalent to: # config is an instance of pyramid.config.Configurator config.add_static_view(name='static', path='mypackage:folder/static', - cachebuster=PathSegmentCacheBuster()) + cachebust=PathSegmentCacheBuster()) :app:`Pyramid` includes two ready to use cache buster implementations: :class:`~pyramid.static.PathSegmentCacheBuster`, which inserts an asset token diff --git a/docs/narr/environment.rst b/docs/narr/environment.rst index 7e2f19278..a81ad19af 100644 --- a/docs/narr/environment.rst +++ b/docs/narr/environment.rst @@ -161,7 +161,7 @@ feature when this is true. Preventing Cache Busting ------------------------ -Prevent the ``cachebuster`` static view configuration argument from having any +Prevent the ``cachebust`` static view configuration argument from having any effect globally in this process when this value is true. No cache buster will be configured or used when this is true. @@ -172,8 +172,8 @@ be configured or used when this is true. +---------------------------------+----------------------------------+ | Environment Variable Name | Config File Setting Name | +=================================+==================================+ -| ``PYRAMID_PREVENT_CACHEBUSTER`` | ``pyramid.prevent_cachebuster`` | -| | or ``prevent_cachebuster`` | +| ``PYRAMID_PREVENT_CACHEBUST`` | ``pyramid.prevent_cachebust`` | +| | or ``prevent_cachebust`` | | | | | | | +---------------------------------+----------------------------------+ diff --git a/pyramid/config/settings.py b/pyramid/config/settings.py index 4d7af6015..492b7d524 100644 --- a/pyramid/config/settings.py +++ b/pyramid/config/settings.py @@ -117,11 +117,11 @@ class Settings(dict): config_prevent_http_cache) eff_prevent_http_cache = asbool(eget('PYRAMID_PREVENT_HTTP_CACHE', config_prevent_http_cache)) - config_prevent_cachebuster = self.get('prevent_cachebuster', '') - config_prevent_cachebuster = self.get('pyramid.prevent_cachebuster', - config_prevent_cachebuster) - eff_prevent_cachebuster = asbool(eget('PYRAMID_PREVENT_CACHEBUSTER', - config_prevent_cachebuster)) + config_prevent_cachebust = self.get('prevent_cachebust', '') + config_prevent_cachebust = self.get('pyramid.prevent_cachebust', + config_prevent_cachebust) + eff_prevent_cachebust = asbool(eget('PYRAMID_PREVENT_CACHEBUST', + config_prevent_cachebust)) update = { 'debug_authorization': eff_debug_all or eff_debug_auth, @@ -133,7 +133,7 @@ class Settings(dict): 'reload_assets':eff_reload_all or eff_reload_assets, 'default_locale_name':eff_locale_name, 'prevent_http_cache':eff_prevent_http_cache, - 'prevent_cachebuster':eff_prevent_cachebuster, + 'prevent_cachebust':eff_prevent_cachebust, 'pyramid.debug_authorization': eff_debug_all or eff_debug_auth, 'pyramid.debug_notfound': eff_debug_all or eff_debug_notfound, @@ -144,7 +144,7 @@ class Settings(dict): 'pyramid.reload_assets':eff_reload_all or eff_reload_assets, 'pyramid.default_locale_name':eff_locale_name, 'pyramid.prevent_http_cache':eff_prevent_http_cache, - 'pyramid.prevent_cachebuster':eff_prevent_cachebuster, + 'pyramid.prevent_cachebust':eff_prevent_cachebust, } self.update(update) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 62feca77e..e6c5baf58 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1783,17 +1783,17 @@ class ViewsConfiguratorMixin(object): Note that this argument has no effect when the ``name`` is a *url prefix*. By default, this argument is ``None``, meaning that no particular Expires or Cache-Control headers are set in the response, - unless ``cachebuster`` is specified. + unless ``cachebust`` is specified. - The ``cachebuster`` keyword argument may be set to cause + The ``cachebust`` keyword argument may be set to cause :meth:`~pyramid.request.Request.static_url` to use cache busting when generating URLs. See :ref:`cache_busting` for general information - about cache busting. The value of the ``cachebuster`` argument may be + about cache busting. The value of the ``cachebust`` argument may be ``True``, in which case a default cache busting implementation is used. - The value of the ``cachebuster`` argument may also be an object which + The value of the ``cachebust`` argument may also be an object which implements :class:`~pyramid.interfaces.ICacheBuster`. See the :mod:`~pyramid.static` module for some implementations. If the - ``cachebuster`` argument is provided, the default for ``cache_max_age`` + ``cachebust`` argument is provided, the default for ``cache_max_age`` is modified to be ten years. ``cache_max_age`` may still be explicitly provided to override this default. @@ -1894,7 +1894,7 @@ def isexception(o): @implementer(IStaticURLInfo) class StaticURLInfo(object): # Indirection for testing - _default_cachebuster = PathSegmentCacheBuster + _default_cachebust = PathSegmentCacheBuster def _get_registrations(self, registry): try: @@ -1909,13 +1909,13 @@ class StaticURLInfo(object): except AttributeError: # bw compat (for tests) registry = get_current_registry() registrations = self._get_registrations(registry) - for (url, spec, route_name, cachebuster) in registrations: + for (url, spec, route_name, cachebust) in registrations: if path.startswith(spec): subpath = path[len(spec):] if WIN: # pragma: no cover subpath = subpath.replace('\\', '/') # windows - if cachebuster: - subpath, kw = cachebuster(subpath, kw) + if cachebust: + subpath, kw = cachebust(subpath, kw) if url is None: kw['subpath'] = subpath return request.route_url(route_name, **kw) @@ -1955,20 +1955,20 @@ class StaticURLInfo(object): # make sure it ends with a slash name = name + '/' - if config.registry.settings.get('pyramid.prevent_cachebuster'): + if config.registry.settings.get('pyramid.prevent_cachebust'): cb = None else: - cb = extra.pop('cachebuster', None) + cb = extra.pop('cachebust', None) if cb is True: - cb = self._default_cachebuster() + cb = self._default_cachebust() if cb: - def cachebuster(subpath, kw): + def cachebust(subpath, kw): token = cb.token(spec + subpath) subpath_tuple = tuple(subpath.split('/')) subpath_tuple, kw = cb.pregenerate(token, subpath_tuple, kw) return '/'.join(subpath_tuple), kw else: - cachebuster = None + cachebust = None if url_parse(name).netloc: # it's a URL @@ -2026,7 +2026,7 @@ class StaticURLInfo(object): registrations.pop(idx) # url, spec, route_name - registrations.append((url, spec, route_name, cachebuster)) + registrations.append((url, spec, route_name, cachebust)) intr = config.introspectable('static views', name, diff --git a/pyramid/tests/test_config/test_settings.py b/pyramid/tests/test_config/test_settings.py index 7cf550c1d..d2a98b347 100644 --- a/pyramid/tests/test_config/test_settings.py +++ b/pyramid/tests/test_config/test_settings.py @@ -131,34 +131,34 @@ class TestSettings(unittest.TestCase): self.assertEqual(result['prevent_http_cache'], True) self.assertEqual(result['pyramid.prevent_http_cache'], True) - def test_prevent_cachebuster(self): + def test_prevent_cachebust(self): settings = self._makeOne({}) - self.assertEqual(settings['prevent_cachebuster'], False) - self.assertEqual(settings['pyramid.prevent_cachebuster'], False) - result = self._makeOne({'prevent_cachebuster':'false'}) - self.assertEqual(result['prevent_cachebuster'], False) - self.assertEqual(result['pyramid.prevent_cachebuster'], False) - result = self._makeOne({'prevent_cachebuster':'t'}) - self.assertEqual(result['prevent_cachebuster'], True) - self.assertEqual(result['pyramid.prevent_cachebuster'], True) - result = self._makeOne({'prevent_cachebuster':'1'}) - self.assertEqual(result['prevent_cachebuster'], True) - self.assertEqual(result['pyramid.prevent_cachebuster'], True) - result = self._makeOne({'pyramid.prevent_cachebuster':'t'}) - self.assertEqual(result['prevent_cachebuster'], True) - self.assertEqual(result['pyramid.prevent_cachebuster'], True) - result = self._makeOne({}, {'PYRAMID_PREVENT_CACHEBUSTER':'1'}) - self.assertEqual(result['prevent_cachebuster'], True) - self.assertEqual(result['pyramid.prevent_cachebuster'], True) - result = self._makeOne({'prevent_cachebuster':'false', - 'pyramid.prevent_cachebuster':'1'}) - self.assertEqual(result['prevent_cachebuster'], True) - self.assertEqual(result['pyramid.prevent_cachebuster'], True) - result = self._makeOne({'prevent_cachebuster':'false', - 'pyramid.prevent_cachebuster':'f'}, - {'PYRAMID_PREVENT_CACHEBUSTER':'1'}) - self.assertEqual(result['prevent_cachebuster'], True) - self.assertEqual(result['pyramid.prevent_cachebuster'], True) + self.assertEqual(settings['prevent_cachebust'], False) + self.assertEqual(settings['pyramid.prevent_cachebust'], False) + result = self._makeOne({'prevent_cachebust':'false'}) + self.assertEqual(result['prevent_cachebust'], False) + self.assertEqual(result['pyramid.prevent_cachebust'], False) + result = self._makeOne({'prevent_cachebust':'t'}) + self.assertEqual(result['prevent_cachebust'], True) + self.assertEqual(result['pyramid.prevent_cachebust'], True) + result = self._makeOne({'prevent_cachebust':'1'}) + self.assertEqual(result['prevent_cachebust'], True) + self.assertEqual(result['pyramid.prevent_cachebust'], True) + result = self._makeOne({'pyramid.prevent_cachebust':'t'}) + self.assertEqual(result['prevent_cachebust'], True) + self.assertEqual(result['pyramid.prevent_cachebust'], True) + result = self._makeOne({}, {'PYRAMID_PREVENT_CACHEBUST':'1'}) + self.assertEqual(result['prevent_cachebust'], True) + self.assertEqual(result['pyramid.prevent_cachebust'], True) + result = self._makeOne({'prevent_cachebust':'false', + 'pyramid.prevent_cachebust':'1'}) + self.assertEqual(result['prevent_cachebust'], True) + self.assertEqual(result['pyramid.prevent_cachebust'], True) + result = self._makeOne({'prevent_cachebust':'false', + 'pyramid.prevent_cachebust':'f'}, + {'PYRAMID_PREVENT_CACHEBUST':'1'}) + self.assertEqual(result['prevent_cachebust'], True) + self.assertEqual(result['pyramid.prevent_cachebust'], True) def test_reload_templates(self): settings = self._makeOne({}) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 8f600c1d2..a0d9ee0c3 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3845,12 +3845,12 @@ class TestStaticURLInfo(unittest.TestCase): self.assertEqual(result, 'http://example.com/abc%20def#La%20Pe%C3%B1a') - def test_generate_url_cachebuster(self): - def cachebuster(subpath, kw): + def test_generate_url_cachebust(self): + def cachebust(subpath, kw): kw['foo'] = 'bar' return 'foo' + '/' + subpath, kw inst = self._makeOne() - registrations = [(None, 'package:path/', '__viewname', cachebuster)] + registrations = [(None, 'package:path/', '__viewname', cachebust)] inst._get_registrations = lambda *x: registrations request = self._makeRequest() def route_url(n, **kw): @@ -3944,28 +3944,28 @@ class TestStaticURLInfo(unittest.TestCase): def test_add_cachebust_default(self): config = self._makeConfig() inst = self._makeOne() - inst._default_cachebuster = DummyCacheBuster - inst.add(config, 'view', 'mypackage:path', cachebuster=True) - cachebuster = config.registry._static_url_registrations[0][3] - subpath, kw = cachebuster('some/path', {}) + inst._default_cachebust = DummyCacheBuster + inst.add(config, 'view', 'mypackage:path', cachebust=True) + cachebust = config.registry._static_url_registrations[0][3] + subpath, kw = cachebust('some/path', {}) self.assertEqual(subpath, 'some/path') self.assertEqual(kw['x'], 'foo') def test_add_cachebust_prevented(self): config = self._makeConfig() - config.registry.settings['pyramid.prevent_cachebuster'] = True + config.registry.settings['pyramid.prevent_cachebust'] = True inst = self._makeOne() - inst.add(config, 'view', 'mypackage:path', cachebuster=True) - cachebuster = config.registry._static_url_registrations[0][3] - self.assertEqual(cachebuster, None) + inst.add(config, 'view', 'mypackage:path', cachebust=True) + cachebust = config.registry._static_url_registrations[0][3] + self.assertEqual(cachebust, None) def test_add_cachebust_custom(self): config = self._makeConfig() inst = self._makeOne() inst.add(config, 'view', 'mypackage:path', - cachebuster=DummyCacheBuster()) - cachebuster = config.registry._static_url_registrations[0][3] - subpath, kw = cachebuster('some/path', {}) + cachebust=DummyCacheBuster()) + cachebust = config.registry._static_url_registrations[0][3] + subpath, kw = cachebust('some/path', {}) self.assertEqual(subpath, 'some/path') self.assertEqual(kw['x'], 'foo') -- cgit v1.2.3 From d47b360cde1d98a67874f88adea69c57ee708dad Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 28 Jul 2014 11:31:45 -0400 Subject: Add test_scaffold_with_hyphen_in_project_name --- pyramid/tests/test_scripts/test_pcreate.py | 87 +++++++++++++++--------------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/pyramid/tests/test_scripts/test_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py index 2488e9595..d2eef2ae9 100644 --- a/pyramid/tests/test_scripts/test_pcreate.py +++ b/pyramid/tests/test_scripts/test_pcreate.py @@ -1,4 +1,6 @@ import unittest +from os import getcwd +from os.path import abspath, join, normpath class TestPCreateCommand(unittest.TestCase): def setUp(self): @@ -50,153 +52,150 @@ class TestPCreateCommand(unittest.TestCase): self.assertTrue(out.startswith('You must provide a project name')) def test_unknown_scaffold_name(self): - cmd = self._makeOne('-s', 'dummyXX', 'distro') + cmd = self._makeOne('-s', 'dummyXX', 'Distro') result = cmd.run() self.assertEqual(result, 2) out = self.out_.getvalue() self.assertTrue(out.startswith('Unavailable scaffolds')) def test_known_scaffold_single_rendered(self): - import os cmd = self._makeOne('-s', 'dummy', 'Distro') scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] - cmd.pyramid_dist = DummyDist("0.1") + cmd.pyramid_dist = DummyDist('0.1') result = cmd.run() self.assertEqual(result, 0) self.assertEqual( - scaffold.output_dir, - os.path.normpath(os.path.join(os.getcwd(), 'Distro')) - ) + scaffold.output_dir, normpath(join(getcwd(), 'Distro'))) self.assertEqual( scaffold.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) + 'pyramid_version': '0.1', 'pyramid_docs_branch': '0.1-branch'}) + + def test_scaffold_with_hyphen_in_project_name(self): + cmd = self._makeOne('-s', 'dummy', 'Distro-') + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + cmd.pyramid_dist = DummyDist('0.1') + result = cmd.run() + self.assertEqual(result, 0) + self.assertEqual( + scaffold.output_dir, normpath(join(getcwd(), 'Distro-'))) + self.assertEqual( + scaffold.vars, + {'project': 'Distro-', 'egg': 'Distro_', 'package': 'distro_', + 'pyramid_version': '0.1', 'pyramid_docs_branch': '0.1-branch'}) def test_known_scaffold_absolute_path(self): - import os - path = os.path.abspath('Distro') + path = abspath('Distro') cmd = self._makeOne('-s', 'dummy', path) - cmd.pyramid_dist = DummyDist("0.1") + cmd.pyramid_dist = DummyDist('0.1') scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] - cmd.pyramid_dist = DummyDist("0.1") + cmd.pyramid_dist = DummyDist('0.1') result = cmd.run() self.assertEqual(result, 0) self.assertEqual( - scaffold.output_dir, - os.path.normpath(os.path.join(os.getcwd(), 'Distro')) - ) + scaffold.output_dir, normpath(join(getcwd(), 'Distro'))) self.assertEqual( scaffold.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) + 'pyramid_version': '0.1', 'pyramid_docs_branch': '0.1-branch'}) def test_known_scaffold_multiple_rendered(self): - import os cmd = self._makeOne('-s', 'dummy1', '-s', 'dummy2', 'Distro') scaffold1 = DummyScaffold('dummy1') scaffold2 = DummyScaffold('dummy2') cmd.scaffolds = [scaffold1, scaffold2] - cmd.pyramid_dist = DummyDist("0.1") + cmd.pyramid_dist = DummyDist('0.1') result = cmd.run() self.assertEqual(result, 0) self.assertEqual( - scaffold1.output_dir, - os.path.normpath(os.path.join(os.getcwd(), 'Distro')) - ) + scaffold1.output_dir, normpath(join(getcwd(), 'Distro'))) self.assertEqual( scaffold1.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) + 'pyramid_version': '0.1', 'pyramid_docs_branch': '0.1-branch'}) self.assertEqual( - scaffold2.output_dir, - os.path.normpath(os.path.join(os.getcwd(), 'Distro')) - ) + scaffold2.output_dir, normpath(join(getcwd(), 'Distro'))) self.assertEqual( scaffold2.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) + 'pyramid_version': '0.1', 'pyramid_docs_branch': '0.1-branch'}) def test_known_scaffold_with_path_as_project_target_rendered(self): - import os cmd = self._makeOne('-s', 'dummy', '/tmp/foo/Distro/') scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] - cmd.pyramid_dist = DummyDist("0.1") + cmd.pyramid_dist = DummyDist('0.1') result = cmd.run() self.assertEqual(result, 0) self.assertEqual( - scaffold.output_dir, - os.path.normpath(os.path.join(os.getcwd(), '/tmp/foo/Distro')) - ) + scaffold.output_dir, normpath(join(getcwd(), '/tmp/foo/Distro'))) self.assertEqual( scaffold.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) - + 'pyramid_version': '0.1', 'pyramid_docs_branch': '0.1-branch'}) def test_scaffold_with_prod_pyramid_version(self): cmd = self._makeOne('-s', 'dummy', 'Distro') scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] - cmd.pyramid_dist = DummyDist("0.2") + cmd.pyramid_dist = DummyDist('0.2') result = cmd.run() self.assertEqual(result, 0) self.assertEqual( scaffold.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.2', 'pyramid_docs_branch':'0.2-branch'}) + 'pyramid_version': '0.2', 'pyramid_docs_branch': '0.2-branch'}) def test_scaffold_with_prod_pyramid_long_version(self): cmd = self._makeOne('-s', 'dummy', 'Distro') scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] - cmd.pyramid_dist = DummyDist("0.2.1") + cmd.pyramid_dist = DummyDist('0.2.1') result = cmd.run() self.assertEqual(result, 0) self.assertEqual( scaffold.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.2.1', 'pyramid_docs_branch':'0.2-branch'}) + 'pyramid_version': '0.2.1', 'pyramid_docs_branch': '0.2-branch'}) def test_scaffold_with_prod_pyramid_unparsable_version(self): cmd = self._makeOne('-s', 'dummy', 'Distro') scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] - cmd.pyramid_dist = DummyDist("abc") + cmd.pyramid_dist = DummyDist('abc') result = cmd.run() self.assertEqual(result, 0) self.assertEqual( scaffold.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': 'abc', 'pyramid_docs_branch':'latest'}) + 'pyramid_version': 'abc', 'pyramid_docs_branch': 'latest'}) def test_scaffold_with_dev_pyramid_version(self): cmd = self._makeOne('-s', 'dummy', 'Distro') scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] - cmd.pyramid_dist = DummyDist("0.12dev") + cmd.pyramid_dist = DummyDist('0.12dev') result = cmd.run() self.assertEqual(result, 0) self.assertEqual( scaffold.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.12dev', - 'pyramid_docs_branch': 'master'}) + 'pyramid_version': '0.12dev', 'pyramid_docs_branch': 'master'}) def test_scaffold_with_dev_pyramid_long_version(self): cmd = self._makeOne('-s', 'dummy', 'Distro') scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] - cmd.pyramid_dist = DummyDist("0.10.1dev") + cmd.pyramid_dist = DummyDist('0.10.1dev') result = cmd.run() self.assertEqual(result, 0) self.assertEqual( scaffold.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.10.1dev', - 'pyramid_docs_branch': 'master'}) + 'pyramid_version': '0.10.1dev', 'pyramid_docs_branch': 'master'}) class Test_main(unittest.TestCase): -- cgit v1.2.3 From f674a8f691d260d44e0f76e3afecfb15484c45b9 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Mon, 28 Jul 2014 17:26:11 -0400 Subject: Mo' features, mo' problems. --- docs/api/static.rst | 7 +++- docs/narr/assets.rst | 87 +++++++++++++++++++++++++++++++------------- pyramid/config/views.py | 4 +- pyramid/static.py | 30 ++++++++++++--- pyramid/tests/test_static.py | 46 +++++++++++++++++++++-- 5 files changed, 135 insertions(+), 39 deletions(-) diff --git a/docs/api/static.rst b/docs/api/static.rst index de5bcabda..543e526ad 100644 --- a/docs/api/static.rst +++ b/docs/api/static.rst @@ -9,8 +9,11 @@ :members: :inherited-members: - .. autoclass:: PathSegmentCacheBuster + .. autoclass:: PathSegmentMd5CacheBuster :members: - .. autoclass:: QueryStringCacheBuster + .. autoclass:: QueryStringMd5CacheBuster + :members: + + .. autoclass:: QueryStringConstantCacheBuster :members: diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 7fb0ec40b..33677988d 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -379,25 +379,19 @@ equivalent to: .. code-block:: python :linenos: - from pyramid.static import PathSegmentCacheBuster + from pyramid.static import PathSegmentMd5CacheBuster # config is an instance of pyramid.config.Configurator config.add_static_view(name='static', path='mypackage:folder/static', - cachebust=PathSegmentCacheBuster()) + cachebust=PathSegmentMd5CacheBuster()) -:app:`Pyramid` includes two ready to use cache buster implementations: -:class:`~pyramid.static.PathSegmentCacheBuster`, which inserts an asset token -in the path portion of the asset's URL, and -:class:`~pyramid.static.QueryStringCacheBuster`, which adds an asset token to -the query string of the asset's URL. Both of these classes generate md5 -checksums as asset tokens. - -.. note:: - - Many HTTP caching proxy implementations will fail to cache any URL which - has a query string. For this reason, you should probably prefer - :class:`~pyramid.static.PathSegmentCacheBuster` to - :class:`~pyramid.static.QueryStringCacheBuster`. +:app:`Pyramid` includes a handful of ready to use cache buster implementations: +:class:`~pyramid.static.PathSegmentMd5CacheBuster`, which inserts an md5 +checksum token in the path portion of the asset's URL, +:class:`~pyramid.static.QueryStringMd5CacheBuster`, which adds an md5 checksum +token to the query string of the asset's URL, and +:class:`~pyramid.static.QueryStringConstantCacheBuster`, which adds an +arbitrary token you provide to the query string of the asset's URL. In order to implement your own cache buster, you can write your own class from scratch which implements the :class:`~pyramid.interfaces.ICacheBuster` @@ -405,22 +399,65 @@ interface. Alternatively you may choose to subclass one of the existing implementations. One of the most likely scenarios is you'd want to change the way the asset token is generated. To do this just subclass an existing implementation and replace the :meth:`~pyramid.interfaces.ICacheBuster.token` -method. Here is an example which just uses a global setting for the asset -token: +method. Here is an example which just uses Git to get the hash of the +currently checked out code: .. code-block:: python :linenos: - - from pyramid.static import PathSegmentCacheBuster - class MyCacheBuster(PathSegmentCacheBuster): - - def __init__(self, config): - # config is an instance of pyramid.config.Configurator - self._token = config.registry.settings['myapp.cachebust_token'] + import os + import subprocess + from pyramid.static import PathSegmentMd5CacheBuster + + class GitCacheBuster(PathSegmentMd5CacheBuster): + """ + Assuming your code is installed as a Git checkout, as opposed to as an + egg from an egg repository like PYPI, you can use this cachebuster to + get the current commit's SHA1 to use as the cache bust token. + """ + def __init__(self): + here = os.path.dirname(os.path.abspath(__file__)) + self.sha1 = subprocess.check_output( + ['git', 'rev-parse', 'HEAD'], + cwd=here).strip() def token(self, pathspec): - return self._token + return self.sha1 + +Choosing a Cache Buster +~~~~~~~~~~~~~~~~~~~~~~~ + +The default cache buster implementation, +:class:`~pyramid.static.PathSegmentMd5CacheBuster`, works very well assuming +that you're using :app:`Pyramid` to serve your static assets. The md5 checksum +is fine grained enough that browsers should only request new versions of +specific assets that have changed. Many caching HTTP proxies will fail to +cache a resource if the URL contains a query string. In general, therefore, +you should prefer a cache busting strategy which modifies the path segment to +a strategy which adds a query string. + +It is possible, however, that your static assets are being served by another +web server or externally on a CDN. In these cases modifying the path segment +for a static asset URL would cause the external service to fail to find the +asset, causing your customer to get a 404. In these cases you would need to +fall back to a cache buster which adds a query string. It is even possible +that there isn't a copy of your static assets available to the :app:`Pyramid` +application, so a cache busting implementation that generates md5 checksums +would fail since it can't access the assets. In such a case, +:class:`~pyramid.static.QueryStringConstantCacheBuster` is a reasonable +fallback. The following code would set up a cachebuster that just uses the +time at start up as a cachebust token: + +.. code-block:: python + :linenos: + + import time + from pyramid.static import QueryStringConstantCacheBuster + + config.add_static_view( + name='http://mycdn.example.com/', + path='mypackage:static', + cachebust=QueryStringConstantCacheBuster(str(time.time()))) .. index:: single: static assets view diff --git a/pyramid/config/views.py b/pyramid/config/views.py index e6c5baf58..5ca696069 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -34,7 +34,7 @@ from pyramid.interfaces import ( ) from pyramid import renderers -from pyramid.static import PathSegmentCacheBuster +from pyramid.static import PathSegmentMd5CacheBuster from pyramid.compat import ( string_types, @@ -1894,7 +1894,7 @@ def isexception(o): @implementer(IStaticURLInfo) class StaticURLInfo(object): # Indirection for testing - _default_cachebust = PathSegmentCacheBuster + _default_cachebust = PathSegmentMd5CacheBuster def _get_registrations(self, registry): try: diff --git a/pyramid/static.py b/pyramid/static.py index 0cbb5533f..5e017e1cd 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -192,7 +192,7 @@ class Md5AssetTokenGenerator(object): self.token_cache[pathspec] = token = _generate_md5(pathspec) return token -class PathSegmentCacheBuster(Md5AssetTokenGenerator): +class PathSegmentMd5CacheBuster(Md5AssetTokenGenerator): """ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which inserts an md5 checksum token for cache busting in the path portion of an @@ -205,17 +205,18 @@ class PathSegmentCacheBuster(Md5AssetTokenGenerator): def match(self, subpath): return subpath[1:] -class QueryStringCacheBuster(Md5AssetTokenGenerator): +class QueryStringMd5CacheBuster(Md5AssetTokenGenerator): """ - An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds a - token for cache busting in the query string of an asset URL. Generated md5 - checksums are cached in order to speed up subsequent calls. + An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds + an md5 checksum token for cache busting in the query string of an asset + URL. Generated md5 checksums are cached in order to speed up subsequent + calls. The optional ``param`` argument determines the name of the parameter added to the query string and defaults to ``'x'``. """ def __init__(self, param='x'): - super(QueryStringCacheBuster, self).__init__() + super(QueryStringMd5CacheBuster, self).__init__() self.param = param def pregenerate(self, token, subpath, kw): @@ -226,4 +227,21 @@ class QueryStringCacheBuster(Md5AssetTokenGenerator): kw['_query'] = tuple(query) + ((self.param, token),) return subpath, kw +class QueryStringConstantCacheBuster(QueryStringMd5CacheBuster): + """ + An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds + an arbitrary token for cache busting in the query string of an asset URL. + + The ``token`` parameter is the token string to use for cache busting and + will be the same for every request. + + The optional ``param`` argument determines the name of the parameter added + to the query string and defaults to ``'x'``. + """ + def __init__(self, token, param='x'): + self._token = token + self.param = param + + def token(self, pathspec): + return self._token diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 134bea25e..2f4de249e 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -420,10 +420,10 @@ class TestMd5AssetTokenGenerator(unittest.TestCase): token = fut(self.fspath) self.assertEqual(token, expected) -class TestPathSegmentCacheBuster(unittest.TestCase): +class TestPathSegmentMd5CacheBuster(unittest.TestCase): def _makeOne(self): - from pyramid.static import PathSegmentCacheBuster as cls + from pyramid.static import PathSegmentMd5CacheBuster as cls inst = cls() inst.token = lambda pathspec: 'foo' return inst @@ -440,10 +440,10 @@ class TestPathSegmentCacheBuster(unittest.TestCase): fut = self._makeOne().match self.assertEqual(fut(('foo', 'bar')), ('bar',)) -class TestQueryStringCacheBuster(unittest.TestCase): +class TestQueryStringMd5CacheBuster(unittest.TestCase): def _makeOne(self, param=None): - from pyramid.static import QueryStringCacheBuster as cls + from pyramid.static import QueryStringMd5CacheBuster as cls if param: inst = cls(param) else: @@ -479,6 +479,44 @@ class TestQueryStringCacheBuster(unittest.TestCase): fut('foo', ('bar',), {'_query': (('a', 'b'),)}), (('bar',), {'_query': (('a', 'b'), ('x', 'foo'))})) +class TestQueryStringConstantCacheBuster(TestQueryStringMd5CacheBuster): + + def _makeOne(self, param=None): + from pyramid.static import QueryStringConstantCacheBuster as cls + if param: + inst = cls('foo', param) + else: + inst = cls('foo') + return inst + + def test_token(self): + fut = self._makeOne().token + self.assertEqual(fut('whatever'), 'foo') + + def test_pregenerate(self): + fut = self._makeOne().pregenerate + self.assertEqual( + fut('foo', ('bar',), {}), + (('bar',), {'_query': {'x': 'foo'}})) + + def test_pregenerate_change_param(self): + fut = self._makeOne('y').pregenerate + self.assertEqual( + fut('foo', ('bar',), {}), + (('bar',), {'_query': {'y': 'foo'}})) + + def test_pregenerate_query_is_already_tuples(self): + fut = self._makeOne().pregenerate + self.assertEqual( + fut('foo', ('bar',), {'_query': [('a', 'b')]}), + (('bar',), {'_query': (('a', 'b'), ('x', 'foo'))})) + + def test_pregenerate_query_is_tuple_of_tuples(self): + fut = self._makeOne().pregenerate + self.assertEqual( + fut('foo', ('bar',), {'_query': (('a', 'b'),)}), + (('bar',), {'_query': (('a', 'b'), ('x', 'foo'))})) + class DummyContext: pass -- cgit v1.2.3 From 6b88bdf7680151345debec0c8651f164a149a53a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 28 Jul 2014 21:06:34 -0400 Subject: add versionadded notes --- docs/narr/assets.rst | 2 ++ docs/narr/environment.rst | 2 ++ pyramid/interfaces.py | 2 ++ pyramid/static.py | 6 ++++++ 4 files changed, 12 insertions(+) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 33677988d..95863848b 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -294,6 +294,8 @@ could be used. Cache Busting ------------- +.. versionadded:: 1.6 + In order to maximize performance of a web application, you generally want to limit the number of times a particular client requests the same static asset. Ideally a client would cache a particular static asset "forever", requiring diff --git a/docs/narr/environment.rst b/docs/narr/environment.rst index a81ad19af..e1171d710 100644 --- a/docs/narr/environment.rst +++ b/docs/narr/environment.rst @@ -165,6 +165,8 @@ Prevent the ``cachebust`` static view configuration argument from having any effect globally in this process when this value is true. No cache buster will be configured or used when this is true. +.. versionadded:: 1.6 + .. seealso:: See also :ref:`cache_busting`. diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 2b6ba7eb6..c5a70dbfd 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1172,6 +1172,8 @@ class ICacheBuster(Interface): a static asset, modifying a static asset URL to include a cache bust token, and, optionally, unmodifying a static asset URL in order to look up an asset. See :ref:`cache_busting`. + + .. versionadded:: 1.6 """ def token(pathspec): """ diff --git a/pyramid/static.py b/pyramid/static.py index 5e017e1cd..c4a9e3cc4 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -198,6 +198,8 @@ class PathSegmentMd5CacheBuster(Md5AssetTokenGenerator): inserts an md5 checksum token for cache busting in the path portion of an asset URL. Generated md5 checksums are cached in order to speed up subsequent calls. + + .. versionadded:: 1.6 """ def pregenerate(self, token, subpath, kw): return (token,) + subpath, kw @@ -214,6 +216,8 @@ class QueryStringMd5CacheBuster(Md5AssetTokenGenerator): The optional ``param`` argument determines the name of the parameter added to the query string and defaults to ``'x'``. + + .. versionadded:: 1.6 """ def __init__(self, param='x'): super(QueryStringMd5CacheBuster, self).__init__() @@ -237,6 +241,8 @@ class QueryStringConstantCacheBuster(QueryStringMd5CacheBuster): The optional ``param`` argument determines the name of the parameter added to the query string and defaults to ``'x'``. + + .. versionadded:: 1.6 """ def __init__(self, token, param='x'): self._token = token -- cgit v1.2.3 From 05ceab495c4390c0e3af5ae6458d4c808eb08e67 Mon Sep 17 00:00:00 2001 From: Dobes Vandermeer Date: Tue, 29 Jul 2014 17:54:23 -0700 Subject: Use None for the default value of the callbacks list. --- pyramid/request.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyramid/request.py b/pyramid/request.py index 7e8b8c07d..fa35dc5f4 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -32,8 +32,8 @@ class TemplateContext(object): pass class CallbackMethodsMixin(object): - response_callbacks = () - finished_callbacks = () + response_callbacks = None + finished_callbacks = None def add_response_callback(self, callback): """ Add a callback to the set of callbacks to be called by the @@ -72,7 +72,7 @@ class CallbackMethodsMixin(object): """ callbacks = self.response_callbacks - if callbacks == (): + if callbacks is None: callbacks = [] callbacks.append(callback) self.response_callbacks = callbacks @@ -132,7 +132,7 @@ class CallbackMethodsMixin(object): """ callbacks = self.finished_callbacks - if callbacks == (): + if callbacks is None: callbacks = [] callbacks.append(callback) self.finished_callbacks = callbacks -- cgit v1.2.3 From c73eb9be17a17c6e0b19ce6b08a579a6d524c584 Mon Sep 17 00:00:00 2001 From: Dobes Vandermeer Date: Tue, 29 Jul 2014 18:05:27 -0700 Subject: Use a deque for the request finished / response callbacks. --- pyramid/request.py | 9 +++++---- pyramid/tests/test_request.py | 25 ++++++++++++++----------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/pyramid/request.py b/pyramid/request.py index fa35dc5f4..bc2889310 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -1,3 +1,4 @@ +from collections import deque import json from zope.interface import implementer @@ -73,14 +74,14 @@ class CallbackMethodsMixin(object): callbacks = self.response_callbacks if callbacks is None: - callbacks = [] + callbacks = deque() callbacks.append(callback) self.response_callbacks = callbacks def _process_response_callbacks(self, response): callbacks = self.response_callbacks while callbacks: - callback = callbacks.pop(0) + callback = callbacks.popleft() callback(self, response) def add_finished_callback(self, callback): @@ -133,14 +134,14 @@ class CallbackMethodsMixin(object): callbacks = self.finished_callbacks if callbacks is None: - callbacks = [] + callbacks = deque() callbacks.append(callback) self.finished_callbacks = callbacks def _process_finished_callbacks(self): callbacks = self.finished_callbacks while callbacks: - callback = callbacks.pop(0) + callback = callbacks.popleft() callback(self) @implementer(IRequest) diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index ec206dad3..48af98f59 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -1,3 +1,4 @@ +from collections import deque import unittest from pyramid import testing @@ -119,13 +120,13 @@ class TestRequest(unittest.TestCase): def test_add_response_callback(self): inst = self._makeOne() - self.assertEqual(inst.response_callbacks, ()) + self.assertEqual(inst.response_callbacks, None) def callback(request, response): """ """ inst.add_response_callback(callback) - self.assertEqual(inst.response_callbacks, [callback]) + self.assertEqual(list(inst.response_callbacks), [callback]) inst.add_response_callback(callback) - self.assertEqual(inst.response_callbacks, [callback, callback]) + self.assertEqual(list(inst.response_callbacks), [callback, callback]) def test__process_response_callbacks(self): inst = self._makeOne() @@ -135,14 +136,15 @@ class TestRequest(unittest.TestCase): def callback2(request, response): request.called2 = True response.called2 = True - inst.response_callbacks = [callback1, callback2] + inst.add_response_callback(callback1) + inst.add_response_callback(callback2) response = DummyResponse() inst._process_response_callbacks(response) self.assertEqual(inst.called1, True) self.assertEqual(inst.called2, True) self.assertEqual(response.called1, True) self.assertEqual(response.called2, True) - self.assertEqual(inst.response_callbacks, []) + self.assertEqual(len(inst.response_callbacks), 0) def test__process_response_callback_adding_response_callback(self): """ @@ -165,17 +167,17 @@ class TestRequest(unittest.TestCase): self.assertEqual(inst.called2, True) self.assertEqual(response.called1, True) self.assertEqual(response.called2, True) - self.assertEqual(inst.response_callbacks, []) + self.assertEqual(len(inst.response_callbacks), 0) def test_add_finished_callback(self): inst = self._makeOne() - self.assertEqual(inst.finished_callbacks, ()) + self.assertEqual(inst.finished_callbacks, None) def callback(request): """ """ inst.add_finished_callback(callback) - self.assertEqual(inst.finished_callbacks, [callback]) + self.assertEqual(list(inst.finished_callbacks), [callback]) inst.add_finished_callback(callback) - self.assertEqual(inst.finished_callbacks, [callback, callback]) + self.assertEqual(list(inst.finished_callbacks), [callback, callback]) def test__process_finished_callbacks(self): inst = self._makeOne() @@ -183,11 +185,12 @@ class TestRequest(unittest.TestCase): request.called1 = True def callback2(request): request.called2 = True - inst.finished_callbacks = [callback1, callback2] + inst.add_finished_callback(callback1) + inst.add_finished_callback(callback2) inst._process_finished_callbacks() self.assertEqual(inst.called1, True) self.assertEqual(inst.called2, True) - self.assertEqual(inst.finished_callbacks, []) + self.assertEqual(len(inst.finished_callbacks), 0) def test_resource_url(self): self._registerResourceURL() -- cgit v1.2.3 From 39a03eef73b14bd05540af70e705d9f02d77f850 Mon Sep 17 00:00:00 2001 From: Dobes Vandermeer Date: Wed, 30 Jul 2014 09:42:28 -0700 Subject: Fix a few tests that assume response_callbacks was a list --- pyramid/tests/test_router.py | 6 +++--- pyramid/tests/test_session.py | 2 +- pyramid/tests/test_testing.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index 838e52db0..c6c6eea1c 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -522,7 +522,7 @@ class TestRouter(unittest.TestCase): def view(context, request): def callback(request, response): response.called_back = True - request.response_callbacks = [callback] + request.add_response_callback(callback) return response environ = self._makeEnviron() self._registerView(view, '', IViewClassifier, IRequest, IContext) @@ -545,7 +545,7 @@ class TestRouter(unittest.TestCase): def view(context, request): def callback(request): request.environ['called_back'] = True - request.finished_callbacks = [callback] + request.add_finished_callback(callback) return response environ = self._makeEnviron() self._registerView(view, '', IViewClassifier, IRequest, IContext) @@ -567,7 +567,7 @@ class TestRouter(unittest.TestCase): def view(context, request): def callback(request): request.environ['called_back'] = True - request.finished_callbacks = [callback] + request.add_finished_callback(callback) raise NotImplementedError environ = self._makeEnviron() self._registerView(view, '', IViewClassifier, IRequest, IContext) diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index 35c234e99..b013ffa66 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -521,7 +521,7 @@ class Test_manage_accessed(unittest.TestCase): result = wrapper(session, 'a') self.assertEqual(result, 1) callbacks = request.response_callbacks - self.assertEqual(len(callbacks), 0) + if callbacks is not None: self.assertEqual(len(callbacks), 0) class Test_manage_changed(unittest.TestCase): def _makeOne(self, wrapped): diff --git a/pyramid/tests/test_testing.py b/pyramid/tests/test_testing.py index 2d0548b33..dfcad2a0c 100644 --- a/pyramid/tests/test_testing.py +++ b/pyramid/tests/test_testing.py @@ -217,7 +217,7 @@ class TestDummyRequest(unittest.TestCase): def test_add_response_callback(self): request = self._makeOne() request.add_response_callback(1) - self.assertEqual(request.response_callbacks, [1]) + self.assertEqual(list(request.response_callbacks), [1]) def test_registry_is_config_registry_when_setup_is_called_after_ctor(self): # see https://github.com/Pylons/pyramid/issues/165 -- cgit v1.2.3 From c1ef71cec60b0bfeac0445c3c30ae46a976f2b31 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 31 Jul 2014 19:37:49 -0400 Subject: Minimize changes to increase merge likelihood --- pyramid/tests/test_scripts/test_pcreate.py | 82 ++++++++++++++++++------------ 1 file changed, 50 insertions(+), 32 deletions(-) diff --git a/pyramid/tests/test_scripts/test_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py index d2eef2ae9..020721ca7 100644 --- a/pyramid/tests/test_scripts/test_pcreate.py +++ b/pyramid/tests/test_scripts/test_pcreate.py @@ -1,6 +1,4 @@ import unittest -from os import getcwd -from os.path import abspath, join, normpath class TestPCreateCommand(unittest.TestCase): def setUp(self): @@ -52,150 +50,170 @@ class TestPCreateCommand(unittest.TestCase): self.assertTrue(out.startswith('You must provide a project name')) def test_unknown_scaffold_name(self): - cmd = self._makeOne('-s', 'dummyXX', 'Distro') + cmd = self._makeOne('-s', 'dummyXX', 'distro') result = cmd.run() self.assertEqual(result, 2) out = self.out_.getvalue() self.assertTrue(out.startswith('Unavailable scaffolds')) def test_known_scaffold_single_rendered(self): + import os cmd = self._makeOne('-s', 'dummy', 'Distro') scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] - cmd.pyramid_dist = DummyDist('0.1') + cmd.pyramid_dist = DummyDist("0.1") result = cmd.run() self.assertEqual(result, 0) self.assertEqual( - scaffold.output_dir, normpath(join(getcwd(), 'Distro'))) + scaffold.output_dir, + os.path.normpath(os.path.join(os.getcwd(), 'Distro')) + ) self.assertEqual( scaffold.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.1', 'pyramid_docs_branch': '0.1-branch'}) + 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) def test_scaffold_with_hyphen_in_project_name(self): + import os cmd = self._makeOne('-s', 'dummy', 'Distro-') scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] - cmd.pyramid_dist = DummyDist('0.1') + cmd.pyramid_dist = DummyDist("0.1") result = cmd.run() self.assertEqual(result, 0) self.assertEqual( - scaffold.output_dir, normpath(join(getcwd(), 'Distro-'))) + scaffold.output_dir, + os.path.normpath(os.path.join(os.getcwd(), 'Distro-')) + ) self.assertEqual( scaffold.vars, {'project': 'Distro-', 'egg': 'Distro_', 'package': 'distro_', - 'pyramid_version': '0.1', 'pyramid_docs_branch': '0.1-branch'}) + 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) def test_known_scaffold_absolute_path(self): - path = abspath('Distro') + import os + path = os.path.abspath('Distro') cmd = self._makeOne('-s', 'dummy', path) - cmd.pyramid_dist = DummyDist('0.1') + cmd.pyramid_dist = DummyDist("0.1") scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] - cmd.pyramid_dist = DummyDist('0.1') + cmd.pyramid_dist = DummyDist("0.1") result = cmd.run() self.assertEqual(result, 0) self.assertEqual( - scaffold.output_dir, normpath(join(getcwd(), 'Distro'))) + scaffold.output_dir, + os.path.normpath(os.path.join(os.getcwd(), 'Distro')) + ) self.assertEqual( scaffold.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.1', 'pyramid_docs_branch': '0.1-branch'}) + 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) def test_known_scaffold_multiple_rendered(self): + import os cmd = self._makeOne('-s', 'dummy1', '-s', 'dummy2', 'Distro') scaffold1 = DummyScaffold('dummy1') scaffold2 = DummyScaffold('dummy2') cmd.scaffolds = [scaffold1, scaffold2] - cmd.pyramid_dist = DummyDist('0.1') + cmd.pyramid_dist = DummyDist("0.1") result = cmd.run() self.assertEqual(result, 0) self.assertEqual( - scaffold1.output_dir, normpath(join(getcwd(), 'Distro'))) + scaffold1.output_dir, + os.path.normpath(os.path.join(os.getcwd(), 'Distro')) + ) self.assertEqual( scaffold1.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.1', 'pyramid_docs_branch': '0.1-branch'}) + 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) self.assertEqual( - scaffold2.output_dir, normpath(join(getcwd(), 'Distro'))) + scaffold2.output_dir, + os.path.normpath(os.path.join(os.getcwd(), 'Distro')) + ) self.assertEqual( scaffold2.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.1', 'pyramid_docs_branch': '0.1-branch'}) + 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) def test_known_scaffold_with_path_as_project_target_rendered(self): + import os cmd = self._makeOne('-s', 'dummy', '/tmp/foo/Distro/') scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] - cmd.pyramid_dist = DummyDist('0.1') + cmd.pyramid_dist = DummyDist("0.1") result = cmd.run() self.assertEqual(result, 0) self.assertEqual( - scaffold.output_dir, normpath(join(getcwd(), '/tmp/foo/Distro'))) + scaffold.output_dir, + os.path.normpath(os.path.join(os.getcwd(), '/tmp/foo/Distro')) + ) self.assertEqual( scaffold.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.1', 'pyramid_docs_branch': '0.1-branch'}) + 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) + def test_scaffold_with_prod_pyramid_version(self): cmd = self._makeOne('-s', 'dummy', 'Distro') scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] - cmd.pyramid_dist = DummyDist('0.2') + cmd.pyramid_dist = DummyDist("0.2") result = cmd.run() self.assertEqual(result, 0) self.assertEqual( scaffold.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.2', 'pyramid_docs_branch': '0.2-branch'}) + 'pyramid_version': '0.2', 'pyramid_docs_branch':'0.2-branch'}) def test_scaffold_with_prod_pyramid_long_version(self): cmd = self._makeOne('-s', 'dummy', 'Distro') scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] - cmd.pyramid_dist = DummyDist('0.2.1') + cmd.pyramid_dist = DummyDist("0.2.1") result = cmd.run() self.assertEqual(result, 0) self.assertEqual( scaffold.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.2.1', 'pyramid_docs_branch': '0.2-branch'}) + 'pyramid_version': '0.2.1', 'pyramid_docs_branch':'0.2-branch'}) def test_scaffold_with_prod_pyramid_unparsable_version(self): cmd = self._makeOne('-s', 'dummy', 'Distro') scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] - cmd.pyramid_dist = DummyDist('abc') + cmd.pyramid_dist = DummyDist("abc") result = cmd.run() self.assertEqual(result, 0) self.assertEqual( scaffold.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': 'abc', 'pyramid_docs_branch': 'latest'}) + 'pyramid_version': 'abc', 'pyramid_docs_branch':'latest'}) def test_scaffold_with_dev_pyramid_version(self): cmd = self._makeOne('-s', 'dummy', 'Distro') scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] - cmd.pyramid_dist = DummyDist('0.12dev') + cmd.pyramid_dist = DummyDist("0.12dev") result = cmd.run() self.assertEqual(result, 0) self.assertEqual( scaffold.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.12dev', 'pyramid_docs_branch': 'master'}) + 'pyramid_version': '0.12dev', + 'pyramid_docs_branch': 'master'}) def test_scaffold_with_dev_pyramid_long_version(self): cmd = self._makeOne('-s', 'dummy', 'Distro') scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] - cmd.pyramid_dist = DummyDist('0.10.1dev') + cmd.pyramid_dist = DummyDist("0.10.1dev") result = cmd.run() self.assertEqual(result, 0) self.assertEqual( scaffold.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', - 'pyramid_version': '0.10.1dev', 'pyramid_docs_branch': 'master'}) + 'pyramid_version': '0.10.1dev', + 'pyramid_docs_branch': 'master'}) class Test_main(unittest.TestCase): -- cgit v1.2.3 From 1a768e3d45594d2458c379bcd55d6f1478ef3281 Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Fri, 8 Aug 2014 23:22:24 -0500 Subject: Link to .ini file description in configuration chapter. The Startup chapter describes a Pyramid application's .ini file. This is now a seealso in the Configuration chapter. --- docs/narr/configuration.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/narr/configuration.rst b/docs/narr/configuration.rst index 52615533d..f7fa94daf 100644 --- a/docs/narr/configuration.rst +++ b/docs/narr/configuration.rst @@ -17,6 +17,10 @@ plugging application code that you've written into :app:`Pyramid` is also referred to within this documentation as "configuration"; you are configuring :app:`Pyramid` to call the code that makes up your application. +.. seealso:: + For information on ``.ini`` files for Pyramid applications see the + :ref:`startup_chapter` chapter. + There are two ways to configure a :app:`Pyramid` application: :term:`imperative configuration` and :term:`declarative configuration`. Both are described below. -- cgit v1.2.3 From ad76ef6ab78847ef86abf97868a32e9604aaab7e Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Fri, 8 Aug 2014 23:28:13 -0500 Subject: Link to logging configuration in the Startup chapter. The Startup chapter describes the application's .ini file. The Logging chapter describes how to configure logging with the .ini file. --- docs/narr/logging.rst | 9 +++++++++ docs/narr/startup.rst | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/docs/narr/logging.rst b/docs/narr/logging.rst index 71029bb33..783fca932 100644 --- a/docs/narr/logging.rst +++ b/docs/narr/logging.rst @@ -294,6 +294,15 @@ use the :term:`pyramid_exclog` package. Details about its configuration are in its `documentation `_. +.. index:: + single: TransLogger + single: middleware; TransLogger + pair: configuration; middleware + single: settings; middleware + pair: .ini; middleware + +.. _request_logging_with_pastes_translogger: + Request Logging with Paste's TransLogger ---------------------------------------- diff --git a/docs/narr/startup.rst b/docs/narr/startup.rst index 7b4a7ea08..cd44e0ee3 100644 --- a/docs/narr/startup.rst +++ b/docs/narr/startup.rst @@ -139,6 +139,13 @@ Here's a high-level time-ordered overview of what happens when you press The server serves the application, and the application is running, waiting to receive requests. +.. seealso:: + Logging configuration is described in the :ref:`logging_chapter` + chapter. There, in :ref:`request_logging_with_pastes_translogger`, + you will also find an example of how to configure + :term:`middleware` to add pre-packaged functionality to your + application. + .. index:: pair: settings; deployment single: custom settings -- cgit v1.2.3 From 4a63f6ac8f19d21eebf23bd8c9f833f2b676287b Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Fri, 8 Aug 2014 23:30:04 -0500 Subject: Add index entries for .ini files vis settings. --- docs/narr/logging.rst | 5 +++++ docs/narr/startup.rst | 1 + 2 files changed, 6 insertions(+) diff --git a/docs/narr/logging.rst b/docs/narr/logging.rst index 783fca932..68da0813c 100644 --- a/docs/narr/logging.rst +++ b/docs/narr/logging.rst @@ -16,6 +16,11 @@ how to send log messages to loggers that you've configured. a third-party scaffold which does not create these files, the configuration information in this chapter may not be applicable. +.. index: + pair: settings; logging + pair: .ini; logging + pair: logging; configuration + .. _logging_config: Logging Configuration diff --git a/docs/narr/startup.rst b/docs/narr/startup.rst index cd44e0ee3..a1a23ed52 100644 --- a/docs/narr/startup.rst +++ b/docs/narr/startup.rst @@ -19,6 +19,7 @@ console. .. index:: single: startup process + pair: settings; .ini The Startup Process ------------------- -- cgit v1.2.3 From dcc6b4aceb140a5b6e03b1d3b0f32d925cbe879c Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Sun, 10 Aug 2014 21:31:01 -0500 Subject: Some improvements to the paste.translogger related docs. Synchronizes with Waitress docs. --- docs/narr/logging.rst | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/docs/narr/logging.rst b/docs/narr/logging.rst index 71029bb33..8f6c74fd4 100644 --- a/docs/narr/logging.rst +++ b/docs/narr/logging.rst @@ -297,12 +297,14 @@ in its `documentation Request Logging with Paste's TransLogger ---------------------------------------- -Paste provides the `TransLogger -`_ :term:`middleware` for -logging requests using the `Apache Combined Log Format -`_. TransLogger combined -with a FileHandler can be used to create an ``access.log`` file similar to -Apache's. +The term:`WSGI` design is modular. Waitress logs error conditions, debugging +output, etc., but not web traffic. For web traffic logging Paste provides the +`TransLogger `_ +:term:`middleware`. TransLogger produces logs in the `Apache Combined Log +Format `_. But +TransLogger does not write to files, the Python logging system must be +configured to do this. The Python FileHandler_ logging handler can be used +alongside TransLogger to create an ``access.log`` file similar to Apache's. Like any standard :term:`middleware` with a Paste entry point, TransLogger can be configured to wrap your application using ``.ini`` file syntax. First, @@ -343,10 +345,12 @@ function of your project's ``__init__`` file: app = TransLogger(app, setup_console_handler=False) return app -TransLogger will automatically setup a logging handler to the console when -called with no arguments, so it 'just works' in environments that don't -configure logging. Since we've configured our own logging handlers, we need -to disable that option via ``setup_console_handler = False``. + +.. note:: + TransLogger will automatically setup a logging handler to the console when + called with no arguments, so it 'just works' in environments that don't + configure logging. Since our logging handlers are configured we disable + the automation via ``setup_console_handler = False``. With the filter in place, TransLogger's logger (named the ``wsgi`` logger) will propagate its log messages to the parent logger (the root logger), sending @@ -361,9 +365,9 @@ its output to the console when we request a page: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.6" -To direct TransLogger to an ``access.log`` FileHandler, we need to add that -FileHandler to the list of handlers (named ``accesslog``), and ensure that the -``wsgi`` logger is configured and uses this handler accordingly: +To direct TransLogger to an ``access.log`` FileHandler, we need the following +to add a FileHandler (named ``accesslog``) to the list of handlers, and ensure +that the ``wsgi`` logger is configured and uses this handler accordingly: .. code-block:: ini @@ -395,7 +399,7 @@ directs its records only to the ``accesslog`` handler. Finally, there's no need to use the ``generic`` formatter with TransLogger as TransLogger itself provides all the information we need. We'll use a formatter that passes-through the log messages as is. Add a new formatter -called ``accesslog`` by include the following in your configuration file: +called ``accesslog`` by including the following in your configuration file: .. code-block:: ini @@ -405,7 +409,9 @@ called ``accesslog`` by include the following in your configuration file: [formatter_accesslog] format = %(message)s -Then wire this new ``accesslog`` formatter into the FileHandler: + +Finally alter the existing configuration to wire this new +``accesslog`` formatter into the FileHandler: .. code-block:: ini -- cgit v1.2.3 From 5cf18393fbe9084e4b079a1136ed5de46ad89969 Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Tue, 12 Aug 2014 20:49:11 -0500 Subject: Docs: Introduce the concept of "userid" into the glossary. --- docs/glossary.rst | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index deb4c1c8b..eb57f3d0d 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -286,13 +286,23 @@ Glossary :term:`authorization policy`. principal - A *principal* is a string or unicode object representing a userid - or a group id. It is provided by an :term:`authentication - policy`. For example, if a user had the user id "bob", and Bob - was part of two groups named "group foo" and "group bar", the - request might have information attached to it that would - indicate that Bob was represented by three principals: "bob", - "group foo" and "group bar". + A *principal* is a string or unicode object representing an + entity, typically a user or group, having zero or more + :term:`permissions `. Principals are provided by an + :term:`authentication policy`. For example, if a user had the + user id "bob", and Bob was part of two groups named "group foo" + and "group bar", the request might have information attached to + it that would indicate that Bob was represented by three + principals: "bob", "group foo" and "group bar". + + userid + A *userid* is a a string or unicode object used to identify and + authenticate a real-world user, often a person. A userid is + supplied to an :term:`authentication policy` in order to discover + the user's :term:`principals `. The default behavior + of the authentication policies :app:`Pyramid` provides is to + return the user's userid as one of the user's principals, but a + userid need not be a principal. authorization policy An authorization policy in :app:`Pyramid` terms is a bit of -- cgit v1.2.3 From 81719b800cfea1c6fd68427ea1d9c0a2f3e6c1dd Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Tue, 12 Aug 2014 21:56:26 -0500 Subject: Docs: Make clear that a userid need not be a principal. --- docs/api/request.rst | 10 ++++++---- docs/narr/security.rst | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/api/request.rst b/docs/api/request.rst index 77d80f6d6..3a32fd938 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -194,10 +194,12 @@ .. versionadded:: 1.5 A property which returns the list of 'effective' :term:`principal` - identifiers for this request. This will include the userid of the - currently authenticated user if a user is currently authenticated. If no - :term:`authentication policy` is in effect, this will return a sequence - containing only the :attr:`pyramid.security.Everyone` principal. + identifiers for this request. This list typically includes the + :term:`userid` of the currently authenticated user if a user is + currently authenticated, but this depends on the + :term:`authentication policy` in effect. If no :term:`authentication + policy` is in effect, this will return a sequence containing only the + :attr:`pyramid.security.Everyone` principal. .. method:: invoke_subrequest(request, use_tweens=False) diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 8db23a33b..57d7ac38f 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -611,9 +611,9 @@ that implements the following interface: def effective_principals(self, request): """ Return a sequence representing the effective principals - including the userid and any groups belonged to by the current - user, including 'system' groups such as - ``pyramid.security.Everyone`` and + typically including the userid and any groups belonged to + by the current user, always including 'system' groups such + as ``pyramid.security.Everyone`` and ``pyramid.security.Authenticated``. """ def remember(self, request, principal, **kw): -- cgit v1.2.3 From c7afe4e43ab19a5e8274988fe8dd004c04c160a1 Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Tue, 12 Aug 2014 22:10:03 -0500 Subject: Security: Change "principal" argument in security.remember() to "userid". Make the change througout the authentication policies, etc. as well. --- docs/narr/security.rst | 14 +++++++------- pyramid/authentication.py | 24 ++++++++++++------------ pyramid/interfaces.py | 4 ++-- pyramid/security.py | 6 +++--- pyramid/testing.py | 4 ++-- pyramid/tests/test_security.py | 4 ++-- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 57d7ac38f..16718cfa4 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -104,9 +104,9 @@ For example: The above configuration enables a policy which compares the value of an "auth ticket" cookie passed in the request's environment which contains a reference -to a single :term:`principal` against the principals present in any -:term:`ACL` found in the resource tree when attempting to call some -:term:`view`. +to a single :term:`userid` and matches that userid's principals against the +principals present in any :term:`ACL` found in the resource tree when +attempting to call some :term:`view`. While it is possible to mix and match different authentication and authorization policies, it is an error to configure a Pyramid application @@ -616,11 +616,11 @@ that implements the following interface: as ``pyramid.security.Everyone`` and ``pyramid.security.Authenticated``. """ - def remember(self, request, principal, **kw): + def remember(self, request, userid, **kw): """ Return a set of headers suitable for 'remembering' the - principal named ``principal`` when set in a response. An - individual authentication policy and its consumers can decide - on the composition and meaning of **kw. """ + userid named ``userid`` when set in a response. An + individual authentication policy and its consumers can + decide on the composition and meaning of **kw. """ def forget(self, request): """ Return a set of headers suitable for 'forgetting' the diff --git a/pyramid/authentication.py b/pyramid/authentication.py index b84981bbc..f4c211ffa 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -335,11 +335,11 @@ class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy): effective_principals.extend(groups) return effective_principals - def remember(self, request, principal, **kw): - """ Store the ``principal`` as ``repoze.who.userid``. + def remember(self, request, userid, **kw): + """ Store the ``userid`` as ``repoze.who.userid``. The identity to authenticated to :mod:`repoze.who` - will contain the given principal as ``userid``, and + will contain the given userid as ``userid``, and provide all keyword arguments as additional identity keys. Useful keys could be ``max_age`` or ``userdata``. """ @@ -348,7 +348,7 @@ class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy): return [] environ = request.environ identity = kw - identity['repoze.who.userid'] = principal + identity['repoze.who.userid'] = userid return identifier.remember(environ, identity) def forget(self, request): @@ -404,7 +404,7 @@ class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy): """ The ``REMOTE_USER`` value found within the ``environ``.""" return request.environ.get(self.environ_key) - def remember(self, request, principal, **kw): + def remember(self, request, userid, **kw): """ A no-op. The ``REMOTE_USER`` does not provide a protocol for remembering the user. This will be application-specific and can be done somewhere else or in a subclass.""" @@ -652,7 +652,7 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): if result: return result['userid'] - def remember(self, request, principal, **kw): + def remember(self, request, userid, **kw): """ Accepts the following kw args: ``max_age=, ``tokens=``. @@ -660,7 +660,7 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): the response. """ - return self.cookie.remember(request, principal, **kw) + return self.cookie.remember(request, userid, **kw) def forget(self, request): """ A list of headers which will delete appropriate cookies.""" @@ -1061,13 +1061,13 @@ class SessionAuthenticationPolicy(CallbackAuthenticationPolicy): self.userid_key = prefix + 'userid' self.debug = debug - def remember(self, request, principal, **kw): - """ Store a principal in the session.""" - request.session[self.userid_key] = principal + def remember(self, request, userid, **kw): + """ Store a userid in the session.""" + request.session[self.userid_key] = userid return [] def forget(self, request): - """ Remove the stored principal from the session.""" + """ Remove the stored userid from the session.""" if self.userid_key in request.session: del request.session[self.userid_key] return [] @@ -1132,7 +1132,7 @@ class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy): if credentials: return credentials[0] - def remember(self, request, principal, **kw): + def remember(self, request, userid, **kw): """ A no-op. Basic authentication does not provide a protocol for remembering the user. Credentials are sent on every request. diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index c5a70dbfd..bba818c8a 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -460,9 +460,9 @@ class IAuthenticationPolicy(Interface): user, including 'system' groups such as Everyone and Authenticated. """ - def remember(request, principal, **kw): + def remember(request, userid, **kw): """ Return a set of headers suitable for 'remembering' the - principal named ``principal`` when set in a response. An + userid named ``userid`` when set in a response. An individual authentication policy and its consumers can decide on the composition and meaning of ``**kw.`` """ diff --git a/pyramid/security.py b/pyramid/security.py index 041155563..3cef7ee5a 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -115,12 +115,12 @@ deprecated( '"effective_principals" attribute of the Pyramid request instead.' ) -def remember(request, principal, **kw): +def remember(request, userid, **kw): """ Returns a sequence of header tuples (e.g. ``[('Set-Cookie', 'foo=abc')]``) on this request's response. These headers are suitable for 'remembering' a set of credentials - implied by the data passed as ``principal`` and ``*kw`` using the + implied by the data passed as ``userid`` and ``*kw`` using the current :term:`authentication policy`. Common usage might look like so within the body of a view function (``response`` is assumed to be a :term:`WebOb` -style :term:`response` object @@ -142,7 +142,7 @@ def remember(request, principal, **kw): policy = _get_authentication_policy(request) if policy is None: return [] - return policy.remember(request, principal, **kw) + return policy.remember(request, userid, **kw) def forget(request): """ diff --git a/pyramid/testing.py b/pyramid/testing.py index 8cbd8b82b..f77889e72 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -79,8 +79,8 @@ class DummySecurityPolicy(object): effective_principals.extend(self.groupids) return effective_principals - def remember(self, request, principal, **kw): - self.remembered = principal + def remember(self, request, userid, **kw): + self.remembered = userid return self.remember_result def forget(self, request): diff --git a/pyramid/tests/test_security.py b/pyramid/tests/test_security.py index 6f08a100c..027f9cda0 100644 --- a/pyramid/tests/test_security.py +++ b/pyramid/tests/test_security.py @@ -462,8 +462,8 @@ class DummyAuthenticationPolicy: def authenticated_userid(self, request): return self.result - def remember(self, request, principal, **kw): - headers = [(_TEST_HEADER, principal)] + def remember(self, request, userid, **kw): + headers = [(_TEST_HEADER, userid)] self._header_remembered = headers[0] return headers -- cgit v1.2.3 From dc324784193a577bc039dcddb0651ef5ec9e6f57 Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Tue, 12 Aug 2014 22:12:25 -0500 Subject: Docs: Make "userid" link to the glossary term. --- docs/api/request.rst | 31 ++++++++++++++++--------------- docs/narr/security.rst | 24 +++++++++++++----------- docs/tutorials/wiki/design.rst | 4 ++-- docs/tutorials/wiki2/design.rst | 3 ++- 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/docs/api/request.rst b/docs/api/request.rst index 3a32fd938..4f93fa34f 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -167,27 +167,28 @@ .. versionadded:: 1.5 - A property which returns the userid of the currently authenticated user - or ``None`` if there is no :term:`authentication policy` in effect or - there is no currently authenticated user. This differs from - :attr:`~pyramid.request.Request.unauthenticated_userid`, because the - effective authentication policy will have ensured that a record - associated with the userid exists in persistent storage; if it has - not, this value will be ``None``. + A property which returns the :term:`userid` of the currently + authenticated user or ``None`` if there is no :term:`authentication + policy` in effect or there is no currently authenticated user. This + differs from :attr:`~pyramid.request.Request.unauthenticated_userid`, + because the effective authentication policy will have ensured that a + record associated with the :term:`userid` exists in persistent storage; if it + has not, this value will be ``None``. .. attribute:: unauthenticated_userid .. versionadded:: 1.5 A property which returns a value which represents the *claimed* (not - verified) user id of the credentials present in the request. ``None`` if - there is no :term:`authentication policy` in effect or there is no user - data associated with the current request. This differs from - :attr:`~pyramid.request.Request.authenticated_userid`, because the - effective authentication policy will not ensure that a record associated - with the userid exists in persistent storage. Even if the userid - does not exist in persistent storage, this value will be the value - of the userid *claimed* by the request data. + verified) :term:`userid` of the credentials present in the + request. ``None`` if there is no :term:`authentication policy` in effect + or there is no user data associated with the current request. This + differs from :attr:`~pyramid.request.Request.authenticated_userid`, + because the effective authentication policy will not ensure that a + record associated with the :term:`userid` exists in persistent storage. + Even if the :term:`userid` does not exist in persistent storage, this + value will be the value of the :term:`userid` *claimed* by the request + data. .. attribute:: effective_principals diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 16718cfa4..f3879d0ba 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -595,19 +595,21 @@ that implements the following interface: """ An object representing a Pyramid authentication policy. """ def authenticated_userid(self, request): - """ Return the authenticated userid or ``None`` if no - authenticated userid can be found. This method of the policy - should ensure that a record exists in whatever persistent store is - used related to the user (the user should not have been deleted); - if a record associated with the current id does not exist in a - persistent store, it should return ``None``.""" + """ Return the authenticated :term:`userid` or ``None`` if + no authenticated userid can be found. This method of the + policy should ensure that a record exists in whatever + persistent store is used related to the user (the user + should not have been deleted); if a record associated with + the current id does not exist in a persistent store, it + should return ``None``.""" def unauthenticated_userid(self, request): - """ Return the *unauthenticated* userid. This method performs the - same duty as ``authenticated_userid`` but is permitted to return the - userid based only on data present in the request; it needn't (and - shouldn't) check any persistent store to ensure that the user record - related to the request userid exists.""" + """ Return the *unauthenticated* userid. This method + performs the same duty as ``authenticated_userid`` but is + permitted to return the userid based only on data present + in the request; it needn't (and shouldn't) check any + persistent store to ensure that the user record related to + the request userid exists.""" def effective_principals(self, request): """ Return a sequence representing the effective principals diff --git a/docs/tutorials/wiki/design.rst b/docs/tutorials/wiki/design.rst index eb785dd1c..28380bd66 100644 --- a/docs/tutorials/wiki/design.rst +++ b/docs/tutorials/wiki/design.rst @@ -53,10 +53,10 @@ Security We'll eventually be adding security to our application. The components we'll use to do this are below. -- USERS, a dictionary mapping usernames to their +- USERS, a dictionary mapping :term:`userids ` to their corresponding passwords. -- GROUPS, a dictionary mapping usernames to a +- GROUPS, a dictionary mapping :term:`userids ` to a list of groups to which they belong to. - ``groupfinder``, an *authorization callback* that looks up diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index df2c83398..ff7413668 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -53,7 +53,8 @@ Security We'll eventually be adding security to our application. The components we'll use to do this are below. -- USERS, a dictionary mapping users names to their corresponding passwords. +- USERS, a dictionary mapping users names (the user's :term:`userids + `) to their corresponding passwords. - GROUPS, a dictionary mapping user names to a list of groups they belong to. -- cgit v1.2.3 From a0cba72fb9925a1476ebf0848fa6ae07bbea5840 Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Tue, 12 Aug 2014 22:33:48 -0500 Subject: Docs: Include the concept of credentials in the high level security overview. --- docs/narr/security.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/narr/security.rst b/docs/narr/security.rst index f3879d0ba..29c62d9f3 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -13,6 +13,11 @@ authorization system can use the credentials in the :term:`request` along with the :term:`context` resource to determine if access will be allowed. Here's how it works at a high level: +- A user may or may not have previously visited the application and + supplied authentication credentials, including a :term:`userid`. If + so, the application may have called + :func:`pyramid.security.remember` to remember these. + - A :term:`request` is generated when a user visits the application. - Based on the request, a :term:`context` resource is located through @@ -25,7 +30,9 @@ allowed. Here's how it works at a high level: context as well as other attributes of the request. - If an :term:`authentication policy` is in effect, it is passed the - request; it returns some number of :term:`principal` identifiers. + request. Based on the request and the remembered (or lack of) + :term:`userid` and related credentials it returns some number of + :term:`principal` identifiers. - If an :term:`authorization policy` is in effect and the :term:`view configuration` associated with the view callable that was found has -- cgit v1.2.3 From 6bedf31e5275c2f2a33051a547aa1dc722aafa97 Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Tue, 12 Aug 2014 23:05:35 -0500 Subject: Docs: Add resource tree into security overview. --- docs/narr/security.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 29c62d9f3..e6bbff44e 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -20,6 +20,12 @@ allowed. Here's how it works at a high level: - A :term:`request` is generated when a user visits the application. +- If an :term:`authorization policy` is in effect the application uses + the request and it's :term:`root factory` to create a :ref:`resource tree + ` of :term:`contexts `. The resource + tree maps contexts to URLs and within the contexts the application + puts declarations which authorize access. + - Based on the request, a :term:`context` resource is located through :term:`resource location`. A context is located differently depending on whether the application uses :term:`traversal` or :term:`URL dispatch`, but -- cgit v1.2.3 From 03e95958a9c2b9042e55bc55e4cdb193649857ef Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Tue, 12 Aug 2014 23:42:20 -0500 Subject: Docs: Switched first 2 paragraphs of security overview. --- docs/narr/security.rst | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/narr/security.rst b/docs/narr/security.rst index e6bbff44e..203962751 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -6,8 +6,18 @@ Security ======== -:app:`Pyramid` provides an optional declarative authorization system -that can prevent a :term:`view` from being invoked based on an +:app:`Pyramid` provides an optional, declarative, security system. +Security in :app:`Pyramid`, unlike many systems, cleanly and explicitly +separates authentication and authorization. Authentication is merely the +mechanism by which credentials provided in the :term:`request` are +resolved to one or more :term:`principal` identifiers. These identifiers +represent the users and groups in effect during the request. +Authorization then determines access based on the :term:`principal` +identifiers, the :term:`view callable` being invoked, and the +:term:`context` resource. + +The :app:`Pyramid` authorization system +can prevent a :term:`view` from being invoked based on an :term:`authorization policy`. Before a view is invoked, the authorization system can use the credentials in the :term:`request` along with the :term:`context` resource to determine if access will be @@ -54,14 +64,6 @@ allowed. Here's how it works at a high level: - If the authorization policy denies access, the view callable is not invoked; instead the :term:`forbidden view` is invoked. -Security in :app:`Pyramid`, unlike many systems, cleanly and explicitly -separates authentication and authorization. Authentication is merely the -mechanism by which credentials provided in the :term:`request` are -resolved to one or more :term:`principal` identifiers. These identifiers -represent the users and groups in effect during the request. -Authorization then determines access based on the :term:`principal` -identifiers, the :term:`view callable` being invoked, and the -:term:`context` resource. Authorization is enabled by modifying your application to include an :term:`authentication policy` and :term:`authorization policy`. -- cgit v1.2.3 From fe83c6bfdab16818cb434d95a09bd6510b43aa24 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 13 Aug 2014 10:48:22 -0500 Subject: some tweaks to the usage of userid in the docs --- docs/api/request.rst | 4 +-- docs/glossary.rst | 15 +++++------ docs/narr/security.rst | 69 ++++++++++++++++++++++++++++---------------------- pyramid/interfaces.py | 57 +++++++++++++++++++++++++++-------------- 4 files changed, 86 insertions(+), 59 deletions(-) diff --git a/docs/api/request.rst b/docs/api/request.rst index 4f93fa34f..dd68fa09c 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -172,8 +172,8 @@ policy` in effect or there is no currently authenticated user. This differs from :attr:`~pyramid.request.Request.unauthenticated_userid`, because the effective authentication policy will have ensured that a - record associated with the :term:`userid` exists in persistent storage; if it - has not, this value will be ``None``. + record associated with the :term:`userid` exists in persistent storage; + if it has not, this value will be ``None``. .. attribute:: unauthenticated_userid diff --git a/docs/glossary.rst b/docs/glossary.rst index eb57f3d0d..ef207a4bb 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -287,22 +287,21 @@ Glossary principal A *principal* is a string or unicode object representing an - entity, typically a user or group, having zero or more - :term:`permissions `. Principals are provided by an + entity, typically a user or group. Principals are provided by an :term:`authentication policy`. For example, if a user had the - user id "bob", and Bob was part of two groups named "group foo" + :term:`userid` `"bob"`, and was part of two groups named `"group foo"` and "group bar", the request might have information attached to it that would indicate that Bob was represented by three - principals: "bob", "group foo" and "group bar". + principals: `"bob"`, `"group foo"` and `"group bar"`. userid - A *userid* is a a string or unicode object used to identify and - authenticate a real-world user, often a person. A userid is + A *userid* is a string or unicode object used to identify and + authenticate a real-world user (or client). A userid is supplied to an :term:`authentication policy` in order to discover the user's :term:`principals `. The default behavior of the authentication policies :app:`Pyramid` provides is to - return the user's userid as one of the user's principals, but a - userid need not be a principal. + return the user's userid as a principal, but this is not strictly + necessary in custom policies that define their principals differently. authorization policy An authorization policy in :app:`Pyramid` terms is a bit of diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 203962751..2dc0c76af 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -7,14 +7,14 @@ Security ======== :app:`Pyramid` provides an optional, declarative, security system. -Security in :app:`Pyramid`, unlike many systems, cleanly and explicitly -separates authentication and authorization. Authentication is merely the -mechanism by which credentials provided in the :term:`request` are -resolved to one or more :term:`principal` identifiers. These identifiers -represent the users and groups in effect during the request. -Authorization then determines access based on the :term:`principal` -identifiers, the :term:`view callable` being invoked, and the -:term:`context` resource. +Security in :app:`Pyramid` is separated into authentication and +authorization. The two systems communicate via :term:`principal` +identifiers. Authentication is merely the mechanism by which credentials +provided in the :term:`request` are resolved to one or more +:term:`principal` identifiers. These identifiers represent the users and +groups that are in effect during the request. Authorization then determines +access based on the :term:`principal` identifiers, the requested +:term:`permission`, and a :term:`context`. The :app:`Pyramid` authorization system can prevent a :term:`view` from being invoked based on an @@ -30,12 +30,6 @@ allowed. Here's how it works at a high level: - A :term:`request` is generated when a user visits the application. -- If an :term:`authorization policy` is in effect the application uses - the request and it's :term:`root factory` to create a :ref:`resource tree - ` of :term:`contexts `. The resource - tree maps contexts to URLs and within the contexts the application - puts declarations which authorize access. - - Based on the request, a :term:`context` resource is located through :term:`resource location`. A context is located differently depending on whether the application uses :term:`traversal` or :term:`URL dispatch`, but @@ -46,9 +40,9 @@ allowed. Here's how it works at a high level: context as well as other attributes of the request. - If an :term:`authentication policy` is in effect, it is passed the - request. Based on the request and the remembered (or lack of) - :term:`userid` and related credentials it returns some number of - :term:`principal` identifiers. + request. It will return some number of :term:`principal` identifiers. + To do this, the policy would need to determine the authenticated + :term:`userid` present in the request. - If an :term:`authorization policy` is in effect and the :term:`view configuration` associated with the view callable that was found has @@ -64,7 +58,6 @@ allowed. Here's how it works at a high level: - If the authorization policy denies access, the view callable is not invoked; instead the :term:`forbidden view` is invoked. - Authorization is enabled by modifying your application to include an :term:`authentication policy` and :term:`authorization policy`. :app:`Pyramid` comes with a variety of implementations of these @@ -119,9 +112,10 @@ For example: The above configuration enables a policy which compares the value of an "auth ticket" cookie passed in the request's environment which contains a reference -to a single :term:`userid` and matches that userid's principals against the -principals present in any :term:`ACL` found in the resource tree when -attempting to call some :term:`view`. +to a single :term:`userid` and matches that userid's +:term:`principals ` against the principals present in any +:term:`ACL` found in the resource tree when attempting to call some +:term:`view`. While it is possible to mix and match different authentication and authorization policies, it is an error to configure a Pyramid application @@ -616,7 +610,9 @@ that implements the following interface: persistent store is used related to the user (the user should not have been deleted); if a record associated with the current id does not exist in a persistent store, it - should return ``None``.""" + should return ``None``. + + """ def unauthenticated_userid(self, request): """ Return the *unauthenticated* userid. This method @@ -624,24 +620,37 @@ that implements the following interface: permitted to return the userid based only on data present in the request; it needn't (and shouldn't) check any persistent store to ensure that the user record related to - the request userid exists.""" + the request userid exists. + + This method is intended primarily a helper to assist the + ``authenticated_userid`` method in pulling credentials out + of the request data, abstracting away the specific headers, + query strings, etc that are used to authenticate the request. + + """ def effective_principals(self, request): """ Return a sequence representing the effective principals - typically including the userid and any groups belonged to - by the current user, always including 'system' groups such + typically including the :term:`userid` and any groups belonged + to by the current user, always including 'system' groups such as ``pyramid.security.Everyone`` and - ``pyramid.security.Authenticated``. """ + ``pyramid.security.Authenticated``. + + """ def remember(self, request, userid, **kw): """ Return a set of headers suitable for 'remembering' the - userid named ``userid`` when set in a response. An + :term:`userid` named ``userid`` when set in a response. An individual authentication policy and its consumers can - decide on the composition and meaning of **kw. """ - + decide on the composition and meaning of **kw. + + """ + def forget(self, request): """ Return a set of headers suitable for 'forgetting' the - current user on subsequent requests. """ + current user on subsequent requests. + + """ After you do so, you can pass an instance of such a class into the :class:`~pyramid.config.Configurator.set_authentication_policy` method diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index bba818c8a..e03704b7f 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -437,38 +437,57 @@ class IViewMapperFactory(Interface): invocation signatures and response values. """ -class IAuthenticationPolicy(Interface): +class IAuthenticationPolicy(object): """ An object representing a Pyramid authentication policy. """ + def authenticated_userid(request): - """ Return the authenticated userid or ``None`` if no authenticated - userid can be found. This method of the policy should ensure that a - record exists in whatever persistent store is used related to the - user (the user should not have been deleted); if a record associated - with the current id does not exist in a persistent store, it should - return ``None``.""" + """ Return the authenticated :term:`userid` or ``None`` if + no authenticated userid can be found. This method of the + policy should ensure that a record exists in whatever + persistent store is used related to the user (the user + should not have been deleted); if a record associated with + the current id does not exist in a persistent store, it + should return ``None``. + + """ def unauthenticated_userid(request): - """ Return the *unauthenticated* userid. This method performs the - same duty as ``authenticated_userid`` but is permitted to return the - userid based only on data present in the request; it needn't (and - shouldn't) check any persistent store to ensure that the user record - related to the request userid exists.""" + """ Return the *unauthenticated* userid. This method + performs the same duty as ``authenticated_userid`` but is + permitted to return the userid based only on data present + in the request; it needn't (and shouldn't) check any + persistent store to ensure that the user record related to + the request userid exists. + + This method is intended primarily a helper to assist the + ``authenticated_userid`` method in pulling credentials out + of the request data, abstracting away the specific headers, + query strings, etc that are used to authenticate the request. + + """ def effective_principals(request): """ Return a sequence representing the effective principals - including the userid and any groups belonged to by the current - user, including 'system' groups such as Everyone and - Authenticated. """ + typically including the :term:`userid` and any groups belonged + to by the current user, always including 'system' groups such + as ``pyramid.security.Everyone`` and + ``pyramid.security.Authenticated``. + + """ def remember(request, userid, **kw): """ Return a set of headers suitable for 'remembering' the - userid named ``userid`` when set in a response. An - individual authentication policy and its consumers can decide - on the composition and meaning of ``**kw.`` """ + :term:`userid` named ``userid`` when set in a response. An + individual authentication policy and its consumers can + decide on the composition and meaning of **kw. + + """ def forget(request): """ Return a set of headers suitable for 'forgetting' the - current user on subsequent requests. """ + current user on subsequent requests. + + """ class IAuthorizationPolicy(Interface): """ An object representing a Pyramid authorization policy. """ -- cgit v1.2.3 From ddc745ff6497a5c08c44e2fc8f722ad0034948dc Mon Sep 17 00:00:00 2001 From: Tim Tisdall Date: Thu, 14 Aug 2014 10:31:22 -0400 Subject: remove unnecessary use of `get_current_registry()` - We have a request object, so get the current registry properly through it. - make use of the built-in `aslist` function for parsing the result --- docs/narr/i18n.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst index 95f663584..3313f8dad 100644 --- a/docs/narr/i18n.rst +++ b/docs/narr/i18n.rst @@ -792,9 +792,11 @@ Then as a part of the code of a custom :term:`locale negotiator`: .. code-block:: python :linenos: - from pyramid.threadlocal import get_current_registry - settings = get_current_registry().settings - languages = settings['available_languages'].split() + from pyramid.settings import aslist + + def my_locale_negotiator(request): + languages = aslist(request.registry.settings['available_languages']) + # ... This is only a suggestion. You can create your own "available languages" configuration scheme as necessary. -- cgit v1.2.3 From e65d6eb43599fe45b8b3978eb78c34ebdf296b66 Mon Sep 17 00:00:00 2001 From: Corey Farwell Date: Sun, 17 Aug 2014 08:11:14 -0700 Subject: Enable automated testing with PyPy 3 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index ce27b5ec3..4ca998c42 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ python: - 3.2 - 3.3 - 3.4 + - pypy3 install: python setup.py dev -- cgit v1.2.3 From 25d8b9790a613f5a0bbdf30b1c3f70b0d212e41a Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 17 Aug 2014 23:39:39 -0700 Subject: - add missing step for tox setup --- HACKING.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/HACKING.txt b/HACKING.txt index 1386be3af..e3afbf241 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -113,6 +113,7 @@ for this use case) and inside that a simple pyramid application named ``hacking`` that you can then fire up like so: cd env27/hacking + ../bin/python setup.py develop ../bin/pserve development.ini Adding Features -- cgit v1.2.3 From befc1b81d7713d5dab130388c285e83d641f7190 Mon Sep 17 00:00:00 2001 From: goodwillcoding Date: Fri, 5 Sep 2014 08:20:29 -0700 Subject: Remove unnecessary call to get_current_registry in NullRendererHelper --- pyramid/renderers.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 108255ee4..e647ebacf 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -248,7 +248,7 @@ class JSON(object): When you've done this, the JSON renderer will be able to serialize instances of the ``Foo`` class when they're encountered in your view results.""" - + self.components.registerAdapter(adapter, (type_or_iface,), IJSONAdapter) @@ -265,7 +265,7 @@ class JSON(object): response.content_type = 'application/json' default = self._make_default(request) return self.serializer(value, default=default, **self.kw) - + return _render def _make_default(self, request): @@ -286,7 +286,7 @@ json_renderer_factory = JSON() # bw compat class JSONP(JSON): """ `JSONP `_ renderer factory helper which implements a hybrid json/jsonp renderer. JSONP is useful for - making cross-domain AJAX requests. + making cross-domain AJAX requests. Configure a JSONP renderer using the :meth:`pyramid.config.Configurator.add_renderer` API at application @@ -309,7 +309,7 @@ class JSONP(JSON): config = Configurator() config.add_renderer('jsonp', JSONP(param_name='callback', indent=4)) - + .. versionchanged:: 1.4 The ability of this class to accept a ``**kw`` in its constructor. @@ -487,18 +487,18 @@ class NullRendererHelper(RendererHelper): @property def settings(self): - return get_current_registry().settings or {} + return {} def render_view(self, request, value, view, context): return value def render(self, value, system_values, request=None): return value - + def render_to_response(self, value, system_values, request=None): return value def clone(self, name=None, package=None, registry=None): return self - + null_renderer = NullRendererHelper() -- cgit v1.2.3 From c4c45446f79d6647aa6a90fc1f45def1319f7ac2 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Wed, 10 Sep 2014 11:15:21 -0600 Subject: Change helloworld to myapp Fix a typo in the documentation. Closes #1408 --- docs/narr/logging.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/logging.rst b/docs/narr/logging.rst index 71029bb33..74d9f260e 100644 --- a/docs/narr/logging.rst +++ b/docs/narr/logging.rst @@ -242,7 +242,7 @@ level is set to ``INFO``, whereas the application's log level is set to [logger_myapp] level = DEBUG handlers = - qualname = helloworld + qualname = myapp All of the child loggers of the ``myapp`` logger will inherit the ``DEBUG`` level unless they're explicitly set differently. Meaning the ``myapp.views``, -- cgit v1.2.3 From 9101510d3c08814e5a77683bf56f66a5b852e44e Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 17 Sep 2014 03:23:42 -0700 Subject: fix typo Re: https://github.com/Pylons/pyramid/pull/1411/files --- docs/quick_tutorial/ini.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/ini.rst b/docs/quick_tutorial/ini.rst index 3402c50e8..132bc30be 100644 --- a/docs/quick_tutorial/ini.rst +++ b/docs/quick_tutorial/ini.rst @@ -16,7 +16,7 @@ This approach is optional, but its presence makes it distinct from other Python web frameworks. It taps into Python's ``setuptools`` library, which establishes conventions for how Python projects can be installed and provide "entry points". Pyramid uses an entry point to -let a Pyramid application it where to find the WSGI app. +let a Pyramid application know where to find the WSGI app. Objectives ========== -- cgit v1.2.3 From ba59b7b87796003138e7ebb01f5bc8a4a7b542a0 Mon Sep 17 00:00:00 2001 From: Omid Raha Date: Mon, 14 Apr 2014 12:38:24 +0430 Subject: Correct missing word. --- docs/quick_tour.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst index 4ab39bb11..41a0dc8c0 100644 --- a/docs/quick_tour.rst +++ b/docs/quick_tour.rst @@ -700,7 +700,7 @@ we might need to detect situations when other people use the site. We need *logging*. Fortunately Pyramid uses the normal Python approach to logging. The -scaffold generated in your ``development.ini`` a number of lines that +scaffold generated in your ``development.ini`` has a number of lines that configure the logging for you to some reasonable defaults. You then see messages sent by Pyramid (for example, when a new request comes in). -- cgit v1.2.3 From fea1750c7a70a666c7bcff40525206397c1080df Mon Sep 17 00:00:00 2001 From: Jay Martin Date: Sat, 20 Sep 2014 11:54:13 -0400 Subject: Update hello_world.rst --- docs/quick_tutorial/hello_world.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/hello_world.rst b/docs/quick_tutorial/hello_world.rst index 1a9ba4c9d..4ae80ca87 100644 --- a/docs/quick_tutorial/hello_world.rst +++ b/docs/quick_tutorial/hello_world.rst @@ -77,7 +77,7 @@ explanation: #. *Lines 12-14*. Use Pyramid's :term:`configurator` to connect :term:`view` code to a particular URL :term:`route`. -#. *Lines 6-7*. Implement the view code that generates the +#. *Lines 6-8*. Implement the view code that generates the :term:`response`. #. *Lines 15-17*. Publish a :term:`WSGI` app using an HTTP -- cgit v1.2.3 From 5f0c50dc5f2d739a816f35992a024f616117b44c Mon Sep 17 00:00:00 2001 From: Jay Martin Date: Fri, 26 Sep 2014 06:38:55 -0400 Subject: Update ini.rst grammar tweak (former discussion:https://github.com/Pylons/pyramid/pull/1413#issuecomment-56891441) --- docs/quick_tutorial/ini.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/quick_tutorial/ini.rst b/docs/quick_tutorial/ini.rst index 132bc30be..b8720711b 100644 --- a/docs/quick_tutorial/ini.rst +++ b/docs/quick_tutorial/ini.rst @@ -14,8 +14,8 @@ Pyramid has a first-class concept of :ref:`configuration ` distinct from code. This approach is optional, but its presence makes it distinct from other Python web frameworks. It taps into Python's ``setuptools`` -library, which establishes conventions for how Python projects can be -installed and provide "entry points". Pyramid uses an entry point to +library, which establishes conventions for installing and providing +"entry points" for Python projects. Pyramid uses an entry point to let a Pyramid application know where to find the WSGI app. Objectives -- cgit v1.2.3 From c92f49b63f005a63a0d9ac33973e7cec84b07c55 Mon Sep 17 00:00:00 2001 From: deisner Date: Mon, 6 Oct 2014 17:01:40 -0400 Subject: Update functional_testing.rst Make it clear that the tests.py file in the functional_testing directory is the one to edit. Also, warn about a potential "gotcha": if the tests.py file is executable, it will be silently ignored. (I ran into this problem while going through the tutorial. This can happen if the file is being edited on a network file share from a different OS, for example.). --- docs/quick_tutorial/functional_testing.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/quick_tutorial/functional_testing.rst b/docs/quick_tutorial/functional_testing.rst index 205ddf5cb..ddcf6b77d 100644 --- a/docs/quick_tutorial/functional_testing.rst +++ b/docs/quick_tutorial/functional_testing.rst @@ -37,12 +37,14 @@ Steps $ $VENV/bin/python setup.py develop $ $VENV/bin/easy_install webtest -#. Let's extend ``unit_testing/tutorial/tests.py`` to include a +#. Let's extend ``functional_testing/tutorial/tests.py`` to include a functional test: .. literalinclude:: functional_testing/tutorial/tests.py :linenos: + Be sure this file is not executable, or ``nosetests`` may not include your tests. + #. Now run the tests: .. code-block:: bash @@ -67,4 +69,4 @@ execution time of our tests. Extra Credit ============ -#. Why do our functional tests use ``b''``? \ No newline at end of file +#. Why do our functional tests use ``b''``? -- cgit v1.2.3 From 34e9381a628d4e5d061d8fe902579a9b9b9cb33c Mon Sep 17 00:00:00 2001 From: David Eisner Date: Mon, 6 Oct 2014 19:27:36 -0400 Subject: Update functional_testing.rst Wrap long line. --- docs/quick_tutorial/functional_testing.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/quick_tutorial/functional_testing.rst b/docs/quick_tutorial/functional_testing.rst index ddcf6b77d..09b05b0bc 100644 --- a/docs/quick_tutorial/functional_testing.rst +++ b/docs/quick_tutorial/functional_testing.rst @@ -43,7 +43,8 @@ Steps .. literalinclude:: functional_testing/tutorial/tests.py :linenos: - Be sure this file is not executable, or ``nosetests`` may not include your tests. + Be sure this file is not executable, or ``nosetests`` may not + include your tests. #. Now run the tests: -- cgit v1.2.3 From 9095de6ac110a7d31ea76a5dbb5b65408897c75e Mon Sep 17 00:00:00 2001 From: David Eisner Date: Wed, 8 Oct 2014 11:46:21 -0400 Subject: Minor edits to Templating With jinja2 * Fix grammar, order of operations in step 1. * Remove Analysis section reference to pyramid.includes in functional tests, which is no longer accurate. --- docs/quick_tutorial/jinja2.rst | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/quick_tutorial/jinja2.rst b/docs/quick_tutorial/jinja2.rst index ad6da7a9e..613542349 100644 --- a/docs/quick_tutorial/jinja2.rst +++ b/docs/quick_tutorial/jinja2.rst @@ -20,8 +20,8 @@ Objectives Steps ===== -#. In this step let's start by installing the ``pyramid_jinja2`` - add-on, the copying the ``view_class`` step's directory: +#. In this step let's start by copying the ``view_class`` step's + directory, and then installing the ``pyramid_jinja2`` add-on. .. code-block:: bash @@ -72,9 +72,6 @@ Our view code stayed largely the same. We simply changed the file extension on the renderer. For the template, the syntax for Chameleon and Jinja2's basic variable insertion is very similar. -Our functional tests don't have ``development.ini`` so they needed the -``pyramid.includes`` to be setup in the test setup. - Extra Credit ============ -- cgit v1.2.3 From ee2313a6842d12ae0db91c14da3855e3f7dd0632 Mon Sep 17 00:00:00 2001 From: Jay Martin Date: Tue, 14 Oct 2014 09:09:36 -0400 Subject: typo fix --- docs/quick_tutorial/forms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/forms.rst b/docs/quick_tutorial/forms.rst index e8bc0c8b4..b08167edc 100644 --- a/docs/quick_tutorial/forms.rst +++ b/docs/quick_tutorial/forms.rst @@ -104,7 +104,7 @@ assets which need to be published. We don't have to know where on disk it is located. We point at the package, then the path inside the package. We just need to include a call to ``add_static_view`` to make that -directory available at a URL. For Pyramid-specific pages, +directory available at a URL. For Pyramid-specific packages, Pyramid provides a facility (``config.include()``) which even makes that unnecessary for consumers of a package. (Deform is not specific to Pyramid.) -- cgit v1.2.3 From 6aa96d5d40f8c420f020ae187d6842e01e6c668c Mon Sep 17 00:00:00 2001 From: Jay Martin Date: Tue, 14 Oct 2014 09:19:47 -0400 Subject: fix route name in comments --- docs/quick_tutorial/more_view_classes/tutorial/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/quick_tutorial/more_view_classes/tutorial/views.py b/docs/quick_tutorial/more_view_classes/tutorial/views.py index 635de0520..fc0ccc37f 100644 --- a/docs/quick_tutorial/more_view_classes/tutorial/views.py +++ b/docs/quick_tutorial/more_view_classes/tutorial/views.py @@ -25,13 +25,13 @@ class TutorialViews: def hello(self): return {'page_title': 'Hello View'} - # Posting to /home via the "Edit" submit button + # Posting to /hello via the "Edit" submit button @view_config(request_method='POST', renderer='edit.pt') def edit(self): new_name = self.request.params['new_name'] return {'page_title': 'Edit View', 'new_name': new_name} - # Posting to /home via the "Delete" submit button + # Posting to /hello via the "Delete" submit button @view_config(request_method='POST', request_param='form.delete', renderer='delete.pt') def delete(self): -- cgit v1.2.3 From ac1bc3cb042fce04696bd799e095722be0d63fb9 Mon Sep 17 00:00:00 2001 From: Jay Martin Date: Tue, 14 Oct 2014 09:09:36 -0400 Subject: typo fix --- docs/quick_tutorial/forms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/forms.rst b/docs/quick_tutorial/forms.rst index e8bc0c8b4..b08167edc 100644 --- a/docs/quick_tutorial/forms.rst +++ b/docs/quick_tutorial/forms.rst @@ -104,7 +104,7 @@ assets which need to be published. We don't have to know where on disk it is located. We point at the package, then the path inside the package. We just need to include a call to ``add_static_view`` to make that -directory available at a URL. For Pyramid-specific pages, +directory available at a URL. For Pyramid-specific packages, Pyramid provides a facility (``config.include()``) which even makes that unnecessary for consumers of a package. (Deform is not specific to Pyramid.) -- cgit v1.2.3 From 2c3ed01554f0fdceeff425b44f7f80c55b9a3545 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 14 Oct 2014 10:03:28 -0500 Subject: fix comment on hello view --- docs/quick_tutorial/more_view_classes/tutorial/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/quick_tutorial/more_view_classes/tutorial/views.py b/docs/quick_tutorial/more_view_classes/tutorial/views.py index fc0ccc37f..156e468a9 100644 --- a/docs/quick_tutorial/more_view_classes/tutorial/views.py +++ b/docs/quick_tutorial/more_view_classes/tutorial/views.py @@ -5,7 +5,7 @@ from pyramid.view import ( @view_defaults(route_name='hello') -class TutorialViews: +class TutorialViews(object): def __init__(self, request): self.request = request self.view_name = 'TutorialViews' @@ -25,13 +25,13 @@ class TutorialViews: def hello(self): return {'page_title': 'Hello View'} - # Posting to /hello via the "Edit" submit button + # Posting to /howdy/first/last via the "Edit" submit button @view_config(request_method='POST', renderer='edit.pt') def edit(self): new_name = self.request.params['new_name'] return {'page_title': 'Edit View', 'new_name': new_name} - # Posting to /hello via the "Delete" submit button + # Posting to /howdy/first/last via the "Delete" submit button @view_config(request_method='POST', request_param='form.delete', renderer='delete.pt') def delete(self): -- cgit v1.2.3 From ae6c883018560fe7b1ef02f21b1ceacf2e75528f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 7 May 2014 22:54:40 -0500 Subject: add config.root_package attribute --- CHANGES.txt | 5 +++++ pyramid/config/__init__.py | 17 +++++++++++++++++ pyramid/tests/test_config/test_init.py | 12 ++++++++++++ 3 files changed, 34 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 63987d980..d66534752 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -7,6 +7,11 @@ Features - Cache busting for static resources has been added and is available via a new argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``. +- Add ``pyramid.config.Configurator.root_package`` attribute and init + parameter to assist with includeable packages that wish to resolve + resources relative to the package in which the ``Configurator`` was created. + See https://github.com/Pylons/pyramid/pull/1337 + Bug Fixes --------- diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index ebaae38a9..cfa35ec6c 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -125,6 +125,14 @@ class Configurator( is passed (the default), the package is assumed to be the Python package in which the *caller* of the ``Configurator`` constructor lives. + If the ``root_package`` is passed, it will propagate through the + configuration hierarchy as a way for included packages to locate + resources relative to the package in which the main ``Configurator`` was + created. If ``None`` is passed (the default), the ``root_package`` will + be derived from the ``package`` argument. The ``package`` attribute is + always pointing at the package being included when using :meth:`.include`, + whereas the ``root_package`` does not change. + If the ``settings`` argument is passed, it should be a Python dictionary representing the :term:`deployment settings` for this application. These are later retrievable using the @@ -243,6 +251,9 @@ class Configurator( .. versionadded:: 1.3 The ``introspection`` argument. + + .. versionadded:: 1.6 + The ``root_package`` argument. """ manager = manager # for testing injection venusian = venusian # for testing injection @@ -272,13 +283,17 @@ class Configurator( exceptionresponse_view=default_exceptionresponse_view, route_prefix=None, introspection=True, + root_package=None, ): if package is None: package = caller_package() + if root_package is None: + root_package = package name_resolver = DottedNameResolver(package) self.name_resolver = name_resolver self.package_name = name_resolver.get_package_name() self.package = name_resolver.get_package() + self.root_package = root_package self.registry = registry self.autocommit = autocommit self.route_prefix = route_prefix @@ -747,6 +762,7 @@ class Configurator( configurator = self.__class__( registry=self.registry, package=package_of(module), + root_package=self.root_package, autocommit=self.autocommit, route_prefix=route_prefix, ) @@ -806,6 +822,7 @@ class Configurator( configurator = self.__class__( registry=self.registry, package=package, + root_package=self.root_package, autocommit=self.autocommit, route_prefix=self.route_prefix, introspection=self.introspection, diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index d6dba17f6..1e58e4d0f 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -736,6 +736,18 @@ pyramid.tests.test_config.dummy_include2""", else: # pragma: no cover raise AssertionError + def test_include_constant_root_package(self): + from pyramid import tests + from pyramid.tests import test_config + config = self._makeOne(root_package=tests) + results = {} + def include(config): + results['package'] = config.package + results['root_package'] = config.root_package + config.include(include) + self.assertEqual(results['root_package'], tests) + self.assertEqual(results['package'], test_config) + def test_action_branching_kw_is_None(self): config = self._makeOne(autocommit=True) self.assertEqual(config.action('discrim'), None) -- cgit v1.2.3 From ab2a77274584700fbcd508d294ff39cb81a99cfd Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 20 Oct 2014 18:06:53 -0500 Subject: reference more pull requests in the change log --- CHANGES.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 63987d980..38c348e67 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -6,22 +6,26 @@ Features - Cache busting for static resources has been added and is available via a new argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``. + See https://github.com/Pylons/pyramid/pull/1380 Bug Fixes --------- - ``pyramid.wsgi.wsgiapp`` and ``pyramid.wsgi.wsgiapp2`` now raise ``ValueError`` when accidentally passed ``None``. + See https://github.com/Pylons/pyramid/pull/1320 - Fix an issue whereby predicates would be resolved as maybe_dotted in the introspectable but not when passed for registration. This would mean that - add_route_predicate for example can not take a string and turn it into the - actual callable function. + ``add_route_predicate`` for example can not take a string and turn it into + the actual callable function. + See https://github.com/Pylons/pyramid/pull/1306 - Fix ``pyramid.testing.setUp`` to return a ``Configurator`` with a proper package. Previously it was not possible to do package-relative includes using the returned ``Configurator`` during testing. There is now a ``package`` argument that can override this behavior as well. + See https://github.com/Pylons/pyramid/pull/1322 - Fix an issue where a ``pyramid.response.FileResponse`` may apply a charset where it does not belong. See https://github.com/Pylons/pyramid/pull/1251 -- cgit v1.2.3 From ba0593aa9488a32193113048f14189b76d374102 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 4 Nov 2014 17:16:27 -0600 Subject: fix squashed docstring on set_request_property fixes #1436 --- pyramid/config/factories.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index 1990c377a..5ce1081c6 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -1,4 +1,4 @@ -from zope.deprecation import deprecate +from zope.deprecation import deprecated from zope.interface import implementer from pyramid.interfaces import ( @@ -180,8 +180,6 @@ class FactoriesConfiguratorMixin(object): introspectables=(intr,)) @action_method - @deprecate('set_request_propery() is deprecated as of Pyramid 1.5; use ' - 'add_request_method() with the property=True argument instead') def set_request_property(self, callable, name=None, reify=False): """ Add a property to the request object. @@ -195,6 +193,11 @@ class FactoriesConfiguratorMixin(object): self.add_request_method( callable, name=name, property=not reify, reify=reify) + deprecated( + set_request_property, + 'set_request_propery() is deprecated as of Pyramid 1.5; use ' + 'add_request_method() with the property=True argument instead') + @implementer(IRequestExtensions) class _RequestExtensions(object): def __init__(self): -- cgit v1.2.3 From a3033c77c423ad2065c1d7ae65b9c61db8ce6f01 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 6 Nov 2014 23:36:41 -0700 Subject: Update documentation for exceptions --- pyramid/httpexceptions.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index ebee39ada..415dfbe7f 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -868,7 +868,12 @@ class HTTPUnprocessableEntity(HTTPClientError): subclass of :class:`~HTTPClientError` This indicates that the server is unable to process the contained - instructions. Only for WebDAV. + instructions. + + May be used to notify the client that their JSON/XML is well formed, but + not correct for the current request. + + See RFC4918 section 11 for more information. code: 422, title: Unprocessable Entity """ @@ -881,7 +886,7 @@ class HTTPLocked(HTTPClientError): """ subclass of :class:`~HTTPClientError` - This indicates that the resource is locked. Only for WebDAV + This indicates that the resource is locked. code: 423, title: Locked """ @@ -896,7 +901,6 @@ class HTTPFailedDependency(HTTPClientError): This indicates that the method could not be performed because the requested action depended on another action and that action failed. - Only for WebDAV. code: 424, title: Failed Dependency """ -- cgit v1.2.3 From 2a079b541b7c917d2be360ea0c4bd0d1ac6a4556 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 9 Nov 2014 11:56:54 -0800 Subject: - add Translation Context term to Glossary to allow Sphinx to build docs, in reference to a recent update in the docstrings to the package translationstring https://github.com/Pylons/translationstring/blame/master/translationstring/__init__.py#L50 --- docs/glossary.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index deb4c1c8b..ef7e9a9ae 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -749,9 +749,16 @@ Glossary made. For example the word "java" might be translated differently if the translation domain is "programming-languages" than would be if the translation domain was "coffee". A - translation domain is represnted by a collection of ``.mo`` files + translation domain is represented by a collection of ``.mo`` files within one or more :term:`translation directory` directories. + Translation Context + A string representing the "context" in which a translation was + made within a given :term:`translation domain`. See the gettext + documentation, `11.2.5 Using contexts for solving ambiguities + `_ + for more information. + Translator A callable which receives a :term:`translation string` and returns a translated Unicode object for the purposes of internationalization. A -- cgit v1.2.3 From 903f48c1e2a9a309b7386f3d69ae20433d0e7dfa Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 9 Nov 2014 20:44:10 -0800 Subject: - rewrap - add missing clause in last paragraph --- docs/quick_tutorial/debugtoolbar.rst | 52 +++++++++++++++++------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/docs/quick_tutorial/debugtoolbar.rst b/docs/quick_tutorial/debugtoolbar.rst index 90750c633..d138eb760 100644 --- a/docs/quick_tutorial/debugtoolbar.rst +++ b/docs/quick_tutorial/debugtoolbar.rst @@ -58,33 +58,31 @@ Steps Analysis ======== -``pyramid_debugtoolbar`` is a full-fledged Python package, -available on PyPI just like thousands of other Python packages. Thus we -start by installing the ``pyramid_debugtoolbar`` package into our -virtual environment using normal Python package installation commands. - -The ``pyramid_debugtoolbar`` Python package is also a Pyramid add-on, -which means we need to include its add-on configuration into our web -application. We could do this with imperative configuration in -``tutorial/__init__.py`` by using ``config.include``. Pyramid also -supports wiring in add-on configuration via our ``development.ini`` -using ``pyramid.includes``. We use this to load the configuration for -the debugtoolbar. - -You'll now see an attractive button on the right side of -your browser, which you may click to provide introspective access to debugging -information in a new browser tab. Even better, if your web application -generates an error, -you will see a nice traceback on the screen. When you want to disable -this toolbar, no need to change code: you can remove it from -``pyramid.includes`` in the relevant ``.ini`` configuration file (thus -showing why configuration files are handy.) - -Note injects a small amount of html/css into your app just before the closing -```` tag in order to display itself. If you -start to experience otherwise inexplicable client-side weirdness, you can shut -it off by commenting out the ``pyramid_debugtoolbar`` line in -``pyramid.includes`` temporarily. +``pyramid_debugtoolbar`` is a full-fledged Python package, available on PyPI +just like thousands of other Python packages. Thus we start by installing the +``pyramid_debugtoolbar`` package into our virtual environment using normal +Python package installation commands. + +The ``pyramid_debugtoolbar`` Python package is also a Pyramid add-on, which +means we need to include its add-on configuration into our web application. We +could do this with imperative configuration in ``tutorial/__init__.py`` by +using ``config.include``. Pyramid also supports wiring in add-on configuration +via our ``development.ini`` using ``pyramid.includes``. We use this to load +the configuration for the debugtoolbar. + +You'll now see an attractive button on the right side of your browser, which +you may click to provide introspective access to debugging information in a +new browser tab. Even better, if your web application generates an error, you +will see a nice traceback on the screen. When you want to disable this +toolbar, no need to change code: you can remove it from ``pyramid.includes`` +in the relevant ``.ini`` configuration file (thus showing why configuration +files are handy.) + +Note that the toolbar injects a small amount of html/css into your app just +before the closing ```` tag in order to display itself. If you start to +experience otherwise inexplicable client-side weirdness, you can shut it off +by commenting out the ``pyramid_debugtoolbar`` line in ``pyramid.includes`` +temporarily. .. seealso:: See also :ref:`pyramid_debugtoolbar `. -- cgit v1.2.3 From 95bf541f94513b5ec2585c5ecf9f9aa684853676 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 9 Nov 2014 20:45:37 -0800 Subject: - add missing "has" --- docs/quick_tutorial/logging.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/logging.rst b/docs/quick_tutorial/logging.rst index 855ded59f..e07d23d6d 100644 --- a/docs/quick_tutorial/logging.rst +++ b/docs/quick_tutorial/logging.rst @@ -16,7 +16,7 @@ we might need to detect problems when other people use the site. We need *logging*. Fortunately Pyramid uses the normal Python approach to logging. The -scaffold generated, in your ``development.ini``, a number of lines that +scaffold generated, in your ``development.ini``, has a number of lines that configure the logging for you to some reasonable defaults. You then see messages sent by Pyramid (for example, when a new request comes in.) -- cgit v1.2.3 From 18566a29e5b3fa88c7ce4732ccb6beee2c8a05c6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 9 Nov 2014 23:41:14 -0600 Subject: update changelog from #1376 --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index d5ad8e094..617e1497c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -40,6 +40,9 @@ Bug Fixes type, unlike any previous version of Python. See https://github.com/Pylons/pyramid/issues/1360 for more information. +- ``pcreate`` now normalizes the package name by converting hyphens to + underscores. See https://github.com/Pylons/pyramid/pull/1376 + Docs ---- -- cgit v1.2.3 From 909486ab9321bbac8018e202a42b290a096d2705 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 9 Nov 2014 23:50:48 -0600 Subject: update changelog for #1373 --- CHANGES.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 617e1497c..0605ed308 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -43,6 +43,10 @@ Bug Fixes - ``pcreate`` now normalizes the package name by converting hyphens to underscores. See https://github.com/Pylons/pyramid/pull/1376 +- Fix an issue with the final response/finished callback being unable to + add another callback to the list. See + https://github.com/Pylons/pyramid/pull/1373 + Docs ---- -- cgit v1.2.3 From 45eb7db00ca472ccd8230f79a01e0eff5c3597ce Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 10 Nov 2014 00:13:32 -0600 Subject: continue #1326 by adding line numbers to development.ini --- pyramid/scaffolds/alchemy/development.ini_tmpl | 2 +- pyramid/scaffolds/starter/development.ini_tmpl | 2 +- pyramid/scaffolds/zodb/development.ini_tmpl | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyramid/scaffolds/alchemy/development.ini_tmpl b/pyramid/scaffolds/alchemy/development.ini_tmpl index e54a8609c..448803c8f 100644 --- a/pyramid/scaffolds/alchemy/development.ini_tmpl +++ b/pyramid/scaffolds/alchemy/development.ini_tmpl @@ -68,4 +68,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/pyramid/scaffolds/starter/development.ini_tmpl b/pyramid/scaffolds/starter/development.ini_tmpl index 842cd61d9..c2a28e178 100644 --- a/pyramid/scaffolds/starter/development.ini_tmpl +++ b/pyramid/scaffolds/starter/development.ini_tmpl @@ -57,4 +57,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/pyramid/scaffolds/zodb/development.ini_tmpl b/pyramid/scaffolds/zodb/development.ini_tmpl index f57d559bf..199ddfab4 100644 --- a/pyramid/scaffolds/zodb/development.ini_tmpl +++ b/pyramid/scaffolds/zodb/development.ini_tmpl @@ -62,4 +62,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s -- cgit v1.2.3 From ba5444531cccf50e7d8b146f80cd22b1508f4bcf Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 10 Nov 2014 00:15:53 -0600 Subject: update changelog --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 0605ed308..927e7ce2a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -13,6 +13,9 @@ Features resources relative to the package in which the ``Configurator`` was created. See https://github.com/Pylons/pyramid/pull/1337 +- Added line numbers to the log formatters in the scaffolds to assist with + debugging. See https://github.com/Pylons/pyramid/pull/1326 + Bug Fixes --------- -- cgit v1.2.3 From 7dd39020afbd50f6b27e03bb81ace700ae280bef Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 10 Nov 2014 00:27:46 -0600 Subject: update changelog with new http exceptions from #1372 --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 927e7ce2a..c1b729b3f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -16,6 +16,11 @@ Features - Added line numbers to the log formatters in the scaffolds to assist with debugging. See https://github.com/Pylons/pyramid/pull/1326 +- Add new HTTP exception objects for status codes + ``428 Precondition Required``, ``429 Too Many Requests`` and + ``431 Request Header Fields Too Large`` in ``pyramid.httpexceptions``. + See https://github.com/Pylons/pyramid/pull/1372/files + Bug Fixes --------- -- cgit v1.2.3 From a9fb5255cedeb02e5c4210b90fe9942c9dbc781a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 10 Nov 2014 01:33:22 -0600 Subject: re-add missing Interface parent --- pyramid/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index e03704b7f..2b56262c0 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -437,7 +437,7 @@ class IViewMapperFactory(Interface): invocation signatures and response values. """ -class IAuthenticationPolicy(object): +class IAuthenticationPolicy(Interface): """ An object representing a Pyramid authentication policy. """ def authenticated_userid(request): -- cgit v1.2.3 From 7a2b72c2ba018d6b75ee151843e37da67bbfc2bb Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 10 Nov 2014 01:34:38 -0600 Subject: update the public api for remember --- docs/api/security.rst | 2 +- pyramid/security.py | 21 ++++++++++++++++++++- pyramid/tests/test_security.py | 17 +++++++++++++++-- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/docs/api/security.rst b/docs/api/security.rst index 814b68e5a..88086dbbf 100644 --- a/docs/api/security.rst +++ b/docs/api/security.rst @@ -16,7 +16,7 @@ Authentication API Functions .. autofunction:: forget -.. autofunction:: remember +.. autofunction:: remember(request, userid, **kwargs) Authorization API Functions --------------------------- diff --git a/pyramid/security.py b/pyramid/security.py index 3cef7ee5a..cbb4b895f 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -17,6 +17,8 @@ Authenticated = 'system.Authenticated' Allow = 'Allow' Deny = 'Deny' +_marker = object() + class AllPermissionsList(object): """ Stand in 'permission list' to represent all permissions """ def __iter__(self): @@ -115,7 +117,7 @@ deprecated( '"effective_principals" attribute of the Pyramid request instead.' ) -def remember(request, userid, **kw): +def remember(request, userid=_marker, **kw): """ Returns a sequence of header tuples (e.g. ``[('Set-Cookie', 'foo=abc')]``) on this request's response. @@ -138,7 +140,24 @@ def remember(request, userid, **kw): always return an empty sequence. If used, the composition and meaning of ``**kw`` must be agreed upon by the calling code and the effective authentication policy. + + .. deprecated:: 1.6 + Renamed the ``principal`` argument to ``userid`` to clarify its + purpose. """ + if userid is _marker: + principal = kw.pop('principal', _marker) + if principal is _marker: + raise TypeError( + 'remember() missing 1 required positional argument: ' + '\'userid\'') + else: + deprecated( + 'principal', + 'The "principal" argument was deprecated in Pyramid 1.6. ' + 'It will be removed in Pyramid 1.9. Use the "userid" ' + 'argument instead.') + userid = principal policy = _get_authentication_policy(request) if policy is None: return [] diff --git a/pyramid/tests/test_security.py b/pyramid/tests/test_security.py index 027f9cda0..6d75ac8e3 100644 --- a/pyramid/tests/test_security.py +++ b/pyramid/tests/test_security.py @@ -134,9 +134,9 @@ class TestRemember(unittest.TestCase): def tearDown(self): testing.tearDown() - def _callFUT(self, *arg): + def _callFUT(self, *arg, **kwarg): from pyramid.security import remember - return remember(*arg) + return remember(*arg, **kwarg) def test_no_authentication_policy(self): request = _makeRequest() @@ -159,6 +159,19 @@ class TestRemember(unittest.TestCase): result = self._callFUT(request, 'me') self.assertEqual(result, [('X-Pyramid-Test', 'me')]) + def test_with_deprecated_principal_arg(self): + request = _makeRequest() + registry = request.registry + _registerAuthenticationPolicy(registry, 'yo') + result = self._callFUT(request, principal='me') + self.assertEqual(result, [('X-Pyramid-Test', 'me')]) + + def test_with_missing_arg(self): + request = _makeRequest() + registry = request.registry + _registerAuthenticationPolicy(registry, 'yo') + self.assertRaises(TypeError, lambda: self._callFUT(request)) + class TestForget(unittest.TestCase): def setUp(self): testing.setUp() -- cgit v1.2.3 From 3ffd40c295a1e37ec94b6123fff8fb4dd5f5abf5 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 10 Nov 2014 01:39:53 -0600 Subject: update changelog --- CHANGES.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index c1b729b3f..8083113ef 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -55,6 +55,13 @@ Bug Fixes add another callback to the list. See https://github.com/Pylons/pyramid/pull/1373 +Deprecations +------------ + +- Renamed the ``principal`` argument to ``pyramid.security.remember()`` to + ``userid`` in order to clarify its intended purpose. + See https://github.com/Pylons/pyramid/pull/1399 + Docs ---- @@ -65,6 +72,10 @@ Docs - Clarify a previously-implied detail of the ``ISession.invalidate`` API documentation. +- Improve and clarify the documentation on what Pyramid defines as a + ``principal`` and a ``userid`` in its security APIs. + See https://github.com/Pylons/pyramid/pull/1399 + Scaffolds --------- -- cgit v1.2.3 From 73b1622d2610fd67ed2f8dd71dac2d9e22d76605 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 10 Nov 2014 01:49:03 -0600 Subject: moar changelogs! --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index c1b729b3f..cf2cced51 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -11,6 +11,9 @@ Features - Add ``pyramid.config.Configurator.root_package`` attribute and init 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. See https://github.com/Pylons/pyramid/pull/1337 - Added line numbers to the log formatters in the scaffolds to assist with -- cgit v1.2.3 From 187fd8ed07693017d743351cfd58f1327c1abb08 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Mon, 10 Nov 2014 01:05:39 -0700 Subject: Change autoclass to autoexception Fixes #1388 or part thereof --- docs/api/exceptions.rst | 12 +++--- docs/api/httpexceptions.rst | 94 ++++++++++++++++++++++----------------------- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/docs/api/exceptions.rst b/docs/api/exceptions.rst index 0c630571f..faca0fbb6 100644 --- a/docs/api/exceptions.rst +++ b/docs/api/exceptions.rst @@ -5,14 +5,14 @@ .. automodule:: pyramid.exceptions - .. autoclass:: BadCSRFToken + .. autoexception:: BadCSRFToken - .. autoclass:: PredicateMismatch + .. autoexception:: PredicateMismatch - .. autoclass:: Forbidden + .. autoexception:: Forbidden - .. autoclass:: NotFound + .. autoexception:: NotFound - .. autoclass:: ConfigurationError + .. autoexception:: ConfigurationError - .. autoclass:: URLDecodeError + .. autoexception:: URLDecodeError diff --git a/docs/api/httpexceptions.rst b/docs/api/httpexceptions.rst index b50f10beb..d4cf97f1d 100644 --- a/docs/api/httpexceptions.rst +++ b/docs/api/httpexceptions.rst @@ -13,96 +13,96 @@ .. autofunction:: exception_response - .. autoclass:: HTTPException + .. autoexception:: HTTPException - .. autoclass:: HTTPOk + .. autoexception:: HTTPOk - .. autoclass:: HTTPRedirection + .. autoexception:: HTTPRedirection - .. autoclass:: HTTPError + .. autoexception:: HTTPError - .. autoclass:: HTTPClientError + .. autoexception:: HTTPClientError - .. autoclass:: HTTPServerError + .. autoexception:: HTTPServerError - .. autoclass:: HTTPCreated + .. autoexception:: HTTPCreated - .. autoclass:: HTTPAccepted + .. autoexception:: HTTPAccepted - .. autoclass:: HTTPNonAuthoritativeInformation + .. autoexception:: HTTPNonAuthoritativeInformation - .. autoclass:: HTTPNoContent + .. autoexception:: HTTPNoContent - .. autoclass:: HTTPResetContent + .. autoexception:: HTTPResetContent - .. autoclass:: HTTPPartialContent + .. autoexception:: HTTPPartialContent - .. autoclass:: HTTPMultipleChoices + .. autoexception:: HTTPMultipleChoices - .. autoclass:: HTTPMovedPermanently + .. autoexception:: HTTPMovedPermanently - .. autoclass:: HTTPFound + .. autoexception:: HTTPFound - .. autoclass:: HTTPSeeOther + .. autoexception:: HTTPSeeOther - .. autoclass:: HTTPNotModified + .. autoexception:: HTTPNotModified - .. autoclass:: HTTPUseProxy + .. autoexception:: HTTPUseProxy - .. autoclass:: HTTPTemporaryRedirect + .. autoexception:: HTTPTemporaryRedirect - .. autoclass:: HTTPBadRequest + .. autoexception:: HTTPBadRequest - .. autoclass:: HTTPUnauthorized + .. autoexception:: HTTPUnauthorized - .. autoclass:: HTTPPaymentRequired + .. autoexception:: HTTPPaymentRequired - .. autoclass:: HTTPForbidden + .. autoexception:: HTTPForbidden - .. autoclass:: HTTPNotFound + .. autoexception:: HTTPNotFound - .. autoclass:: HTTPMethodNotAllowed + .. autoexception:: HTTPMethodNotAllowed - .. autoclass:: HTTPNotAcceptable + .. autoexception:: HTTPNotAcceptable - .. autoclass:: HTTPProxyAuthenticationRequired + .. autoexception:: HTTPProxyAuthenticationRequired - .. autoclass:: HTTPRequestTimeout + .. autoexception:: HTTPRequestTimeout - .. autoclass:: HTTPConflict + .. autoexception:: HTTPConflict - .. autoclass:: HTTPGone + .. autoexception:: HTTPGone - .. autoclass:: HTTPLengthRequired + .. autoexception:: HTTPLengthRequired - .. autoclass:: HTTPPreconditionFailed + .. autoexception:: HTTPPreconditionFailed - .. autoclass:: HTTPRequestEntityTooLarge + .. autoexception:: HTTPRequestEntityTooLarge - .. autoclass:: HTTPRequestURITooLong + .. autoexception:: HTTPRequestURITooLong - .. autoclass:: HTTPUnsupportedMediaType + .. autoexception:: HTTPUnsupportedMediaType - .. autoclass:: HTTPRequestRangeNotSatisfiable + .. autoexception:: HTTPRequestRangeNotSatisfiable - .. autoclass:: HTTPExpectationFailed + .. autoexception:: HTTPExpectationFailed - .. autoclass:: HTTPUnprocessableEntity + .. autoexception:: HTTPUnprocessableEntity - .. autoclass:: HTTPLocked + .. autoexception:: HTTPLocked - .. autoclass:: HTTPFailedDependency + .. autoexception:: HTTPFailedDependency - .. autoclass:: HTTPInternalServerError + .. autoexception:: HTTPInternalServerError - .. autoclass:: HTTPNotImplemented + .. autoexception:: HTTPNotImplemented - .. autoclass:: HTTPBadGateway + .. autoexception:: HTTPBadGateway - .. autoclass:: HTTPServiceUnavailable + .. autoexception:: HTTPServiceUnavailable - .. autoclass:: HTTPGatewayTimeout + .. autoexception:: HTTPGatewayTimeout - .. autoclass:: HTTPVersionNotSupported + .. autoexception:: HTTPVersionNotSupported - .. autoclass:: HTTPInsufficientStorage + .. autoexception:: HTTPInsufficientStorage -- cgit v1.2.3 From 7a76cd0b183d5080ec863a7d494008e65469f683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domen=20Ko=C5=BEar?= Date: Tue, 11 Nov 2014 08:02:09 +0100 Subject: fixes #1405 --- pyramid/tests/test_response.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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): -- cgit v1.2.3 From f10d1e8f9e7a8d65218b9fb09efe3b6fa9511bdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domen=20Ko=C5=BEar?= Date: Thu, 12 Dec 2013 21:14:10 +0100 Subject: if view argument is not passed to config.add_notfound_view, use default_exceptionresponse_view --- pyramid/config/views.py | 7 +++++++ pyramid/tests/test_config/test_views.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 5ca696069..fbe7fc712 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 ( @@ -1671,6 +1672,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 +1701,9 @@ class ViewsConfiguratorMixin(object): % arg ) + if not view: + view = default_exceptionresponse_view + settings = dict( view=view, context=HTTPNotFound, diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index a0d9ee0c3..0fb7c734a 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1860,6 +1860,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) -- cgit v1.2.3 From 41ba4dfda4dd0e7fb5c32aaa34c164fe3ad43142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domen=20Ko=C5=BEar?= Date: Tue, 11 Feb 2014 22:02:55 +0100 Subject: properly detect undefined view --- pyramid/config/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index fbe7fc712..6de5646f6 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1701,7 +1701,7 @@ class ViewsConfiguratorMixin(object): % arg ) - if not view: + if view is None: view = default_exceptionresponse_view settings = dict( -- cgit v1.2.3 From dfa449126a8cb87c58e6e7519df5aecf252d5127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domen=20Ko=C5=BEar?= Date: Tue, 11 Nov 2014 07:50:58 +0100 Subject: if view argument is not passed to config.add_forbidden_view, use default_exceptionresponse_view --- pyramid/config/views.py | 8 +++++++- pyramid/tests/test_config/test_views.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 6de5646f6..e4171b0c5 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1592,9 +1592,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``, @@ -1610,6 +1613,9 @@ class ViewsConfiguratorMixin(object): % arg ) + if view is None: + view = default_exceptionresponse_view + settings = dict( view=view, context=HTTPForbidden, diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 0fb7c734a..39b8ba70d 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) -- cgit v1.2.3 From 46a268b3e0c3f80974bc9f4471afdc819ba28763 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 11 Nov 2014 01:15:23 -0600 Subject: update changelog --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index cf2cced51..4d697de64 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -58,6 +58,9 @@ 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 + Docs ---- -- cgit v1.2.3 From 7b1d4223db73163f46600cf3d3badf4961dddafb Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 11 Nov 2014 01:19:36 -0600 Subject: update changelog --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 4d697de64..b5d08c8ff 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -24,6 +24,12 @@ Features ``431 Request Header Fields Too Large`` in ``pyramid.httpexceptions``. See https://github.com/Pylons/pyramid/pull/1372/files +- 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 + Bug Fixes --------- -- cgit v1.2.3 From f3a5679992c51ed3067bb6f5b577dad9fe4274ff Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 11 Nov 2014 01:34:37 -0600 Subject: enable PYTHONSTARTUP support in pshell Fixes #1299 --- CHANGES.txt | 4 ++++ pyramid/scripts/pshell.py | 10 ++++++++++ pyramid/tests/test_scripts/pystartup.py | 1 + pyramid/tests/test_scripts/test_pshell.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+) create mode 100644 pyramid/tests/test_scripts/pystartup.py diff --git a/CHANGES.txt b/CHANGES.txt index cf2cced51..4bd438bd7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -24,6 +24,10 @@ Features ``431 Request Header Fields Too Large`` in ``pyramid.httpexceptions``. See https://github.com/Pylons/pyramid/pull/1372/files +- The ``pshell`` script will now load a ``PYTHONSTARTUP`` file if one is + defined in the environment prior to launching the interpreter. + See https://github.com/Pylons/pyramid/pull/1299 + Bug Fixes --------- diff --git a/pyramid/scripts/pshell.py b/pyramid/scripts/pshell.py index 12b078677..ef462239b 100644 --- a/pyramid/scripts/pshell.py +++ b/pyramid/scripts/pshell.py @@ -1,9 +1,11 @@ from code import interact import optparse +import os import sys import textwrap from pyramid.compat import configparser +from pyramid.compat import exec_ from pyramid.util import DottedNameResolver from pyramid.paster import bootstrap @@ -51,10 +53,12 @@ class PShellCommand(object): loaded_objects = {} object_help = {} setup = None + pystartup = None def __init__(self, argv, quiet=False): self.quiet = quiet self.options, self.args = self.parser.parse_args(argv[1:]) + self.pystartup = os.environ.get('PYTHONSTARTUP') def pshell_file_config(self, filename): config = self.ConfigParser() @@ -144,6 +148,12 @@ class PShellCommand(object): if shell is None: shell = self.make_shell() + if self.pystartup and os.path.isfile(self.pystartup): + with open(self.pystartup, 'rb') as fp: + exec_(fp.read().decode('utf-8'), env) + if '__builtins__' in env: + del env['__builtins__'] + try: shell(env, help) finally: diff --git a/pyramid/tests/test_scripts/pystartup.py b/pyramid/tests/test_scripts/pystartup.py new file mode 100644 index 000000000..c4e5bcc80 --- /dev/null +++ b/pyramid/tests/test_scripts/pystartup.py @@ -0,0 +1 @@ +foo = 1 diff --git a/pyramid/tests/test_scripts/test_pshell.py b/pyramid/tests/test_scripts/test_pshell.py index 7cb130c41..86858a709 100644 --- a/pyramid/tests/test_scripts/test_pshell.py +++ b/pyramid/tests/test_scripts/test_pshell.py @@ -369,6 +369,34 @@ class TestPShellCommand(unittest.TestCase): self.assertTrue(self.bootstrap.closer.called) self.assertTrue(shell.help) + def test_command_loads_pythonstartup(self): + import os + marker = object() + old_pystartup = os.environ.get('PYTHONSTARTUP', marker) + os.environ['PYTHONSTARTUP'] = ( + os.path.abspath( + os.path.join( + os.path.dirname(__file__), + 'pystartup.py'))) + try: + command = self._makeOne() + shell = dummy.DummyShell() + command.run(shell) + self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') + self.assertEqual(shell.env, { + 'app':self.bootstrap.app, 'root':self.bootstrap.root, + 'registry':self.bootstrap.registry, + 'request':self.bootstrap.request, + 'root_factory':self.bootstrap.root_factory, + 'foo':1, + }) + self.assertTrue(self.bootstrap.closer.called) + self.assertTrue(shell.help) + finally: # pragma: no cover + if old_pystartup is not marker: + os.environ['PYTHONSTARTUP'] = old_pystartup + else: + del os.environ['PYTHONSTARTUP'] class Test_main(unittest.TestCase): def _callFUT(self, argv): -- cgit v1.2.3 From 940a7a3e3a254ba3b5db333f2a07ab43f5018d98 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Thu, 10 Jul 2014 16:29:29 -0700 Subject: add failing test for package root spec static view --- pyramid/tests/test_config/test_views.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 39b8ba70d..a82f7f257 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3898,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)] + self._assertRegistrations(config, expected) + def test_add_url_withendslash(self): inst = self._makeOne() config = self._makeConfig() -- cgit v1.2.3 From e7745ac72ff5c5c499722a8cfcc589a77201fc9a Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Thu, 10 Jul 2014 16:31:31 -0700 Subject: Fix static views with package root spec patterns --- CHANGES.txt | 3 +++ pyramid/config/views.py | 2 +- pyramid/tests/test_config/test_views.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b5d08c8ff..5a0edc566 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -67,6 +67,9 @@ Bug Fixes - 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/pyramid/config/views.py b/pyramid/config/views.py index e4171b0c5..ba3981388 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1955,7 +1955,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/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index a82f7f257..b0d03fb72 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3902,7 +3902,7 @@ class TestStaticURLInfo(unittest.TestCase): inst = self._makeOne() config = self._makeConfig() inst.add(config, 'http://example.com', 'package:') - expected = [('http://example.com/', 'package:', None)] + expected = [('http://example.com/', 'package:', None, None)] self._assertRegistrations(config, expected) def test_add_url_withendslash(self): -- cgit v1.2.3 From 0b0ea0a6fff1d238bcc419c7a4feb72ad4969175 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Tue, 11 Nov 2014 00:42:43 -0800 Subject: Add myself to contributors --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) 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 -- cgit v1.2.3 From 555969e05458b2e19305fd0a4f15ac3d27d3a90c Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 11 Nov 2014 00:50:29 -0800 Subject: fix grammar --- docs/narr/i18n.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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``. -- cgit v1.2.3 From e51dd09eb1ba4c873f7dec763a1e51c5779801b7 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Tue, 11 Nov 2014 08:17:02 -0800 Subject: Format proutes output and include module instead of repr of view --- pyramid/scripts/proutes.py | 66 ++++++++++++++++++++++++++---- pyramid/tests/test_scripts/test_proutes.py | 10 +++-- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index 5784026bb..792030a74 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -4,11 +4,17 @@ import textwrap from pyramid.paster import bootstrap from pyramid.scripts.common import parse_vars +from pyramid.config.views import MultiView + + +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 +49,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') @@ -59,13 +65,22 @@ 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 +88,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, '')) + view_callable = '' 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 isinstance(view_callable, MultiView): + 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_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py index 25a3cd2e3..45ab57d3a 100644 --- a/pyramid/tests/test_scripts/test_proutes.py +++ b/pyramid/tests/test_scripts/test_proutes.py @@ -123,8 +123,11 @@ 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', ' Date: Tue, 11 Nov 2014 12:01:10 -0600 Subject: adjust tests to work even when someone has defined PYTHONSTARTUP in their shell --- CHANGES.txt | 2 +- pyramid/scripts/pshell.py | 3 +-- pyramid/tests/test_scripts/test_pshell.py | 41 ++++++++++++++----------------- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 4bd438bd7..f72a793a5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -26,7 +26,7 @@ Features - The ``pshell`` script will now load a ``PYTHONSTARTUP`` file if one is defined in the environment prior to launching the interpreter. - See https://github.com/Pylons/pyramid/pull/1299 + See https://github.com/Pylons/pyramid/pull/1448 Bug Fixes --------- diff --git a/pyramid/scripts/pshell.py b/pyramid/scripts/pshell.py index ef462239b..1168ba78a 100644 --- a/pyramid/scripts/pshell.py +++ b/pyramid/scripts/pshell.py @@ -53,12 +53,11 @@ class PShellCommand(object): loaded_objects = {} object_help = {} setup = None - pystartup = None + pystartup = os.environ.get('PYTHONSTARTUP') def __init__(self, argv, quiet=False): self.quiet = quiet self.options, self.args = self.parser.parse_args(argv[1:]) - self.pystartup = os.environ.get('PYTHONSTARTUP') def pshell_file_config(self, filename): config = self.ConfigParser() diff --git a/pyramid/tests/test_scripts/test_pshell.py b/pyramid/tests/test_scripts/test_pshell.py index 86858a709..a6ba2eaea 100644 --- a/pyramid/tests/test_scripts/test_pshell.py +++ b/pyramid/tests/test_scripts/test_pshell.py @@ -1,3 +1,4 @@ +import os import unittest from pyramid.tests.test_scripts import dummy @@ -24,6 +25,9 @@ class TestPShellCommand(unittest.TestCase): self.options.python_shell = '' self.options.setup = None cmd.options = self.options + # default to None to prevent side-effects from running tests in + # unknown environments + cmd.pystartup = None return cmd def test_make_default_shell(self): @@ -370,33 +374,24 @@ class TestPShellCommand(unittest.TestCase): self.assertTrue(shell.help) def test_command_loads_pythonstartup(self): - import os - marker = object() - old_pystartup = os.environ.get('PYTHONSTARTUP', marker) - os.environ['PYTHONSTARTUP'] = ( + command = self._makeOne() + command.pystartup = ( os.path.abspath( os.path.join( os.path.dirname(__file__), 'pystartup.py'))) - try: - command = self._makeOne() - shell = dummy.DummyShell() - command.run(shell) - self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') - self.assertEqual(shell.env, { - 'app':self.bootstrap.app, 'root':self.bootstrap.root, - 'registry':self.bootstrap.registry, - 'request':self.bootstrap.request, - 'root_factory':self.bootstrap.root_factory, - 'foo':1, - }) - self.assertTrue(self.bootstrap.closer.called) - self.assertTrue(shell.help) - finally: # pragma: no cover - if old_pystartup is not marker: - os.environ['PYTHONSTARTUP'] = old_pystartup - else: - del os.environ['PYTHONSTARTUP'] + shell = dummy.DummyShell() + command.run(shell) + self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') + self.assertEqual(shell.env, { + 'app':self.bootstrap.app, 'root':self.bootstrap.root, + 'registry':self.bootstrap.registry, + 'request':self.bootstrap.request, + 'root_factory':self.bootstrap.root_factory, + 'foo':1, + }) + self.assertTrue(self.bootstrap.closer.called) + self.assertTrue(shell.help) class Test_main(unittest.TestCase): def _callFUT(self, argv): -- cgit v1.2.3 From 0a50d16f44885ec3aee3044981ebb5c6081c3657 Mon Sep 17 00:00:00 2001 From: Christophe de Vienne Date: Mon, 4 Aug 2014 15:08:29 +0200 Subject: Remove duplicate code --- pyramid/config/views.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index e4171b0c5..db67d2582 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1186,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, -- cgit v1.2.3 From c617b7df97a326ca010ddb196978169e2a178c4a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 13 Nov 2014 15:47:21 -0600 Subject: update changelog for fixes from #1453 --- CHANGES.txt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b5d08c8ff..76f9bc84e 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 @@ -30,6 +30,9 @@ Features ``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 + Bug Fixes --------- -- cgit v1.2.3 From 716a20fc79c98e250c90a3d3e9f2218bec181a8d Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 16 Nov 2014 23:11:15 -0600 Subject: use hmac.compare_digest if available --- CHANGES.txt | 5 +++++ pyramid/tests/test_util.py | 43 +++++++++++++++++++++++++++++++++++++++++++ pyramid/util.py | 32 ++++++++++++++++++++++++-------- 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index a893ebae4..bbaa6739e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -33,6 +33,11 @@ Features - 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 --------- 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): -- cgit v1.2.3 From 8d535290147bc943045e1feaee9326cf3b996bed Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 16 Nov 2014 23:31:11 -0600 Subject: attempt to clarify the benefit of these changes --- CHANGES.txt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 7d79ddd18..2e2707f3f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -8,7 +8,17 @@ Features argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``. - Assets can now be overidden by an absolute path on the filesystem when using - the ``config.override_asset`` API. + the ``config.override_asset`` API. This makes it possible to fully support + serving up static content from a mutable directory while still being able + to use the ``request.static_url`` API and ``config.add_static_view``. + Previously it was not possible to use ``config.add_static_view`` with an + absolute path **and** generate urls to the content. This change replaces + the call, ``config.add_static_view('/abs/path', 'static')``, with + ``config.add_static_view('myapp:static', 'static')`` and + ``config.override_asset(to_override='myapp:static/', + override_with='/abs/path/')``. The ``myapp:static`` asset spec is completely + made up and does not need to exist - it is used for generating urls + via ``request.static_url('myapp:static/foo.png')``. See https://github.com/Pylons/pyramid/issues/1229 Bug Fixes -- cgit v1.2.3 From e0c09c151ffb9bce0fdc71fb351745e3c282bb18 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 16 Nov 2014 22:15:15 -0800 Subject: Make sure tox fails the cover build if it isn't at 100% --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 -- cgit v1.2.3 From 36046388d5cbe99b8d972853efba03b2fb5aa203 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 16 Nov 2014 22:58:22 -0800 Subject: Added coverage for MultiView and long names in proutes --- pyramid/scripts/proutes.py | 6 ++- pyramid/tests/test_scripts/test_proutes.py | 83 ++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index 792030a74..d0c1aa13e 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -4,7 +4,6 @@ import textwrap from pyramid.paster import bootstrap from pyramid.scripts.common import parse_vars -from pyramid.config.views import MultiView PAD = 3 @@ -58,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] @@ -72,6 +73,7 @@ class PRoutesCommand(object): max_view = len('View') routes = mapper.get_routes() + if not routes: return 0 @@ -97,7 +99,7 @@ class PRoutesCommand(object): IView, name='', default=None) if view_callable is not None: - if isinstance(view_callable, MultiView): + if IMultiView.providedBy(view_callable): view_callables = [ x[1] for x in view_callable.views ] diff --git a/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py index 45ab57d3a..32202af4b 100644 --- a/pyramid/tests/test_scripts/test_proutes.py +++ b/pyramid/tests/test_scripts/test_proutes.py @@ -128,6 +128,48 @@ class TestPRoutesCommand(unittest.TestCase): ['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 @@ -157,6 +199,47 @@ class TestPRoutesCommand(unittest.TestCase): self.assertEqual(len(L), 3) self.assertEqual(L[-1].split()[:3], ['a', '/a', '']) + 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 -- cgit v1.2.3 From 6bdda352153a277fb2812746dce5522f441a49f2 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 16 Nov 2014 23:08:25 -0800 Subject: Switch to using tox for travis so coverage is ran --- .travis.yml | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4ca998c42..2d54a2b36 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: + - TOX_ENV=py26 + - TOX_ENV=py27 + - TOX_ENV=py32 + - TOX_ENV=py33 + - TOX_ENV=py34 + - TOX_ENV=pypy + - TOX_ENV=cover -install: python setup.py dev +install: + - pip install tox -script: python setup.py test -q +script: + - tox -e $TOX_ENV notifications: email: -- cgit v1.2.3 From b542a4d723e5e994f693884618878186b94fa51c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 17 Nov 2014 01:10:11 -0600 Subject: improve the docs for absolute path overrides --- docs/narr/assets.rst | 65 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 74708ff3e..fc908c2b4 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -276,15 +276,62 @@ to put static media on a separate webserver during production (if the ``name`` argument to :meth:`~pyramid.config.Configurator.add_static_view` is a URL), while keeping static media package-internal and served by the development webserver during development (if the ``name`` argument to -:meth:`~pyramid.config.Configurator.add_static_view` is a URL prefix). To -create such a circumstance, we suggest using the -:attr:`pyramid.registry.Registry.settings` API in conjunction with a setting -in the application ``.ini`` file named ``media_location``. Then set the -value of ``media_location`` to either a prefix or a URL depending on whether -the application is being run in development or in production (use a different -``.ini`` file for production than you do for development). This is just a -suggestion for a pattern; any setting name other than ``media_location`` -could be used. +:meth:`~pyramid.config.Configurator.add_static_view` is a URL prefix). + +For example, we may define a :ref:`custom setting ` +named ``media_location`` which we can set to an external URL in production +when our assets are hosted on a CDN. + +.. code-block:: python + :linenos: + + media_location = settings.get('media_location', 'static') + + config = Configurator(settings=settings) + config.add_static_view(path='myapp:static', name=media_location) + +Now we can optionally define the setting in our ini file: + +.. code-block:: ini + :linenos: + + # production.ini + [app:main] + use = egg:myapp#main + + media_location = http://static.example.com/ + +It is also possible to serve assets that live outside of the source by +referring to an absolute path on the filesystem. There are two ways to +accomplish this. + +First, :meth:`~pyramid.config.Configurator.add_static_view` +supports taking an absolute path directly instead of an asset spec. This works +as expected, looking in the file or folder of files and serving them up at +some URL within your application or externally. Unfortunately, this technique +has a drawback that it is not possible to use the +:meth:`~pyramid.request.Request.static_url` method to generate URLs, since it +works based on an asset spec. + +The second approach, available in Pyramid 1.6+, uses the asset overriding +APIs described in the :ref:`overriding_assets_section` section. It is then +possible to configure a "dummy" package which then serves its file or folder +from an absolute path. + +.. code-block:: python + + config.add_static_view(path='myapp:static_images', name='static') + config.override_asset(to_override='myapp:static_images/', + override_with='/abs/path/to/images/') + +From this configuration it is now possible to use +:meth:`~pyramid.request.Request.static_url` to generate URLs to the data +in the folder by doing something like +``request.static_url('myapp:static_images/foo.png')``. While it is not +necessary that the ``static_images`` file or folder actually exist in the +``myapp`` package, it is important that the ``myapp`` portion points to a +valid package. If the folder does exist then the overriden folder is given +priority if the file's name exists in both locations. .. index:: single: Cache Busting -- cgit v1.2.3 From d965c4fa42aa04888e5a829d9975ffec26037c9b Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 16 Nov 2014 23:21:37 -0800 Subject: Use travis_retry in case of timeouts, remove -e $OTX_ENV --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2d54a2b36..dddeb1df7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,10 +11,10 @@ env: - TOX_ENV=cover install: - - pip install tox + - travis_retry pip install tox script: - - tox -e $TOX_ENV + - travis_retry tox notifications: email: -- cgit v1.2.3 From 650d3d5fa383d89a3b28029162d6ef4d58be3da1 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 17 Nov 2014 01:25:58 -0600 Subject: reference appropriate PR --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 2e2707f3f..06852b885 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -19,7 +19,7 @@ Features override_with='/abs/path/')``. The ``myapp:static`` asset spec is completely made up and does not need to exist - it is used for generating urls via ``request.static_url('myapp:static/foo.png')``. - See https://github.com/Pylons/pyramid/issues/1229 + See https://github.com/Pylons/pyramid/issues/1252 Bug Fixes --------- -- cgit v1.2.3 From 6beffc41634844f3ea3b6152f292d3dbe6b5500c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 17 Nov 2014 01:29:25 -0600 Subject: note the deprecation in the todo --- TODO.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/TODO.txt b/TODO.txt index 62b8c39f4..e738b58d8 100644 --- a/TODO.txt +++ b/TODO.txt @@ -125,7 +125,10 @@ Future - 1.7: Change ``pyramid.authentication.AuthTktAuthenticationPolicy`` default ``hashalg`` to ``sha512``. -- 1.8 Remove set_request_property. +- 1.8: Remove set_request_property. + +- 1.9: Remove extra code enabling ``pyramid.security.remember(principal=...)`` + and force use of ``userid``. Probably Bad Ideas ------------------ -- cgit v1.2.3 From 1d298deae192918a994423c3fc4ee9cd4bf7e7ca Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 17 Nov 2014 01:50:09 -0600 Subject: fix travis.yml to use the correct TOXENV var --- .travis.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index dddeb1df7..4ff4939d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,13 +2,13 @@ language: python env: - - TOX_ENV=py26 - - TOX_ENV=py27 - - TOX_ENV=py32 - - TOX_ENV=py33 - - TOX_ENV=py34 - - TOX_ENV=pypy - - TOX_ENV=cover + - TOXENV=py26 + - TOXENV=py27 + - TOXENV=py32 + - TOXENV=py33 + - TOXENV=py34 + - TOXENV=pypy + - TOXENV=cover install: - travis_retry pip install tox -- cgit v1.2.3 From d10b97733618b92e1b6cfe7cbb8802f90b58dcde Mon Sep 17 00:00:00 2001 From: John Anderson Date: Mon, 17 Nov 2014 00:37:58 -0800 Subject: Add RTD and Travis badges in the README --- README.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.rst b/README.rst index a3458028b..73709319c 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,13 @@ Pyramid ======= +.. image:: https://travis-ci.org/Pylons/pyramid.png?branch=master + :target: https://travis-ci.org/Pylons/pyramid + +.. image:: https://readthedocs.org/projects/pyramid/badge/?version=latest + :target: https://readthedocs.org/projects/pyramid/?badge=latest + :alt: Documentation Status + Pyramid is a small, fast, down-to-earth, open source Python web framework. It makes real-world web application development and deployment more fun, more predictable, and more productive. -- cgit v1.2.3 From 39a18291c45fa7b7591dec42ba22a5ad1957a86f Mon Sep 17 00:00:00 2001 From: John Anderson Date: Mon, 17 Nov 2014 00:39:25 -0800 Subject: Use direct link to docs --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 73709319c..c6f174adf 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ Pyramid :target: https://travis-ci.org/Pylons/pyramid .. image:: https://readthedocs.org/projects/pyramid/badge/?version=latest - :target: https://readthedocs.org/projects/pyramid/?badge=latest + :target: http://pyramid.readthedocs.org/en/latest/ :alt: Documentation Status Pyramid is a small, fast, down-to-earth, open source Python web framework. -- cgit v1.2.3 From 0760606f7b6af51a4431e336f46541485387efd5 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 17 Nov 2014 08:36:59 -0500 Subject: Don't fail coverage check on systems w/ locale set. See: http://jenkins.pylonsproject.org/job/pyramid/1617/console. --- pyramid/tests/test_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py index 35648ed38..c2786c391 100644 --- a/pyramid/tests/test_integration.py +++ b/pyramid/tests/test_integration.py @@ -81,7 +81,7 @@ class StaticAppBase(IntegrationBase): res = self.testapp.get('/static/.hiddenfile', status=200) _assertBody(res.body, os.path.join(here, 'fixtures/static/.hiddenfile')) - if defaultlocale is not None: + if defaultlocale is not None: # pragma: no cover # 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 -- cgit v1.2.3 From 5f75af8e760559dc321836f6f7bec4c147b1de42 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Mon, 17 Nov 2014 06:58:34 -0800 Subject: Use pylonsproject.org link for docs, point to master branch --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index c6f174adf..adf7eea5e 100644 --- a/README.rst +++ b/README.rst @@ -4,8 +4,8 @@ Pyramid .. image:: https://travis-ci.org/Pylons/pyramid.png?branch=master :target: https://travis-ci.org/Pylons/pyramid -.. image:: https://readthedocs.org/projects/pyramid/badge/?version=latest - :target: http://pyramid.readthedocs.org/en/latest/ +.. image:: https://readthedocs.org/projects/pyramid/badge/?version=master + :target: http://docs.pylonsproject.org/projects/pyramid/en/master/ :alt: Documentation Status Pyramid is a small, fast, down-to-earth, open source Python web framework. -- cgit v1.2.3 From bbbcfb35a55685b3b90dcfdfb32853b81639d126 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 17 Nov 2014 10:50:54 -0800 Subject: - add step for badges in README.rst --- RELEASING.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASING.txt b/RELEASING.txt index 553d2dcf2..0adef552c 100644 --- a/RELEASING.txt +++ b/RELEASING.txt @@ -26,6 +26,9 @@ Releasing Pyramid - Copy relevant changes (delta bug fixes) from CHANGES.txt to docs/whatsnew-X.X (if it's a major release). +- update README.rst to use correct versions of badges and URLs according to + each branch and context, i.e., RTD "latest" == GitHub/Travis "1.x-branch". + - Make sure docs render OK:: $ cd docs -- cgit v1.2.3 From c62f7dc452f262ed747e0038d3962e8a198ee1d5 Mon Sep 17 00:00:00 2001 From: Matt Russell Date: Wed, 19 Nov 2014 19:32:25 +0000 Subject: Fixes breaking docs build due to unquoted asterisks in doc string. --- pyramid/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 2b56262c0..b21c6b9cc 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -479,7 +479,7 @@ class IAuthenticationPolicy(Interface): """ Return a set of headers suitable for 'remembering' the :term:`userid` named ``userid`` when set in a response. An individual authentication policy and its consumers can - decide on the composition and meaning of **kw. + decide on the composition and meaning of ``**kw``. """ -- cgit v1.2.3 From 39c7e4fec6265355b3eefb77a5dfbf9d6fc26ded Mon Sep 17 00:00:00 2001 From: Matt Russell Date: Wed, 19 Nov 2014 23:20:01 +0000 Subject: Correct url in git remote command. --- HACKING.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HACKING.txt b/HACKING.txt index e3afbf241..16c17699c 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -31,7 +31,7 @@ By Hand $ cd hack-on-pyramid # Configure remotes such that you can pull changes from the Pyramid # repository into your local repository. - $ git remote add upstream https://github.com:Pylons/pyramid.git + $ git remote add upstream https://github.com/Pylons/pyramid.git # fetch and merge changes from upstream into master $ git fetch upstream $ git merge upstream/master -- cgit v1.2.3 From bc8e4d2acf583b9c581202c0f0efee3ee1da710a Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Fri, 21 Nov 2014 14:26:10 -0500 Subject: Add support for testing 'pypy3' under Tox / Travis. --- .travis.yml | 1 + CHANGES.txt | 2 ++ tox.ini | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4ff4939d9..5a205b268 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ env: - TOXENV=py33 - TOXENV=py34 - TOXENV=pypy + - TOXENV=pypy3 - TOXENV=cover install: diff --git a/CHANGES.txt b/CHANGES.txt index c7c829fb6..ea3323aa0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,8 @@ Next release Features -------- +- Added support / testing for 'pypy3' under Tox and Travis. + - Cache busting for static resources has been added and is available via a new argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``. See https://github.com/Pylons/pyramid/pull/1380 diff --git a/tox.ini b/tox.ini index 9a9c5a983..3f32dbc3f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py26,py27,py32,py33,py34,pypy,cover + py26,py27,py32,py33,py34,pypy,pypy3,cover [testenv] commands = -- cgit v1.2.3 From cf4ad5eaea4af3da3bbac4c421fe5f959ed1d256 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 18:54:20 -0600 Subject: update tox/travis to check code coverage on py3 coverage is combined between py2 and py3 for an aggregate coverage metric. This means we can stop putting "no cover" gates around py3 code and ignoring whether it's ever tested. --- .travis.yml | 3 +- pyramid/compat.py | 45 +++++++++++++++--------------- pyramid/i18n.py | 8 +++--- pyramid/scaffolds/tests.py | 4 +-- pyramid/tests/test_config/test_adapters.py | 2 +- pyramid/tests/test_path.py | 2 +- pyramid/tests/test_request.py | 2 +- pyramid/tests/test_scripts/test_pserve.py | 2 +- pyramid/tests/test_traversal.py | 2 +- pyramid/tests/test_urldispatch.py | 2 +- pyramid/tests/test_util.py | 4 +-- pyramid/traversal.py | 2 +- pyramid/urldispatch.py | 2 +- pyramid/util.py | 2 +- setup.cfg | 1 - tox.ini | 12 ++++---- 16 files changed, 48 insertions(+), 47 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5a205b268..482d2a910 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: python env: + - TOXENV=clean - TOXENV=py26 - TOXENV=py27 - TOXENV=py32 @@ -9,7 +10,7 @@ env: - TOXENV=py34 - TOXENV=pypy - TOXENV=pypy3 - - TOXENV=cover + - TOXENV=report install: - travis_retry pip install tox diff --git a/pyramid/compat.py b/pyramid/compat.py index bfa345b88..919a6d244 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -23,7 +23,7 @@ except ImportError: # pragma: no cover # True if we are running on Python 3. PY3 = sys.version_info[0] == 3 -if PY3: # pragma: no cover +if PY3: string_types = str, integer_types = int, class_types = type, @@ -43,16 +43,16 @@ def text_(s, encoding='latin-1', errors='strict'): ``s.decode(encoding, errors)``, otherwise return ``s``""" if isinstance(s, binary_type): return s.decode(encoding, errors) - return s # pragma: no cover + return s def bytes_(s, encoding='latin-1', errors='strict'): """ If ``s`` is an instance of ``text_type``, return ``s.encode(encoding, errors)``, otherwise return ``s``""" - if isinstance(s, text_type): # pragma: no cover + if isinstance(s, text_type): return s.encode(encoding, errors) return s -if PY3: # pragma: no cover +if PY3: def ascii_native_(s): if isinstance(s, text_type): s = s.encode('ascii') @@ -72,7 +72,7 @@ Python 2: If ``s`` is an instance of ``text_type``, return """ -if PY3: # pragma: no cover +if PY3: def native_(s, encoding='latin-1', errors='strict'): """ If ``s`` is an instance of ``text_type``, return ``s``, otherwise return ``str(s, encoding, errors)``""" @@ -95,7 +95,7 @@ Python 2: If ``s`` is an instance of ``text_type``, return ``s.encode(encoding, errors)``, otherwise return ``str(s)`` """ -if PY3: # pragma: no cover +if PY3: from urllib import parse urlparse = parse from urllib.parse import quote as url_quote @@ -169,13 +169,13 @@ else: # pragma: no cover return d.iterkeys() -if PY3: # pragma: no cover +if PY3: def map_(*arg): return list(map(*arg)) else: map_ = map -if PY3: # pragma: no cover +if PY3: def is_nonstr_iter(v): if isinstance(v, str): return False @@ -184,45 +184,44 @@ else: def is_nonstr_iter(v): return hasattr(v, '__iter__') -if PY3: # pragma: no cover +if PY3: im_func = '__func__' im_self = '__self__' else: im_func = 'im_func' im_self = 'im_self' -try: # pragma: no cover +try: import configparser -except ImportError: # pragma: no cover +except ImportError: import ConfigParser as configparser try: - from Cookie import SimpleCookie -except ImportError: # pragma: no cover from http.cookies import SimpleCookie +except ImportError: + from Cookie import SimpleCookie -if PY3: # pragma: no cover +if PY3: from html import escape else: from cgi import escape -try: # pragma: no cover - input_ = raw_input -except NameError: # pragma: no cover +if PY3: input_ = input +else: + input_ = raw_input - -try: - from StringIO import StringIO as NativeIO -except ImportError: # pragma: no cover +if PY3: from io import StringIO as NativeIO +else: + from io import BytesIO as NativeIO # "json" is not an API; it's here to support older pyramid_debugtoolbar # versions which attempt to import it import json -if PY3: # pragma: no cover +if PY3: # see PEP 3333 for why we encode WSGI PATH_INFO to latin-1 before # decoding it to utf-8 def decode_path_info(path): @@ -231,7 +230,7 @@ else: def decode_path_info(path): return path.decode('utf-8') -if PY3: # pragma: no cover +if PY3: # see PEP 3333 for why we decode the path to latin-1 from urllib.parse import unquote_to_bytes def unquote_bytes_to_wsgi(bytestring): diff --git a/pyramid/i18n.py b/pyramid/i18n.py index 4c8f4b55d..c30351f7a 100644 --- a/pyramid/i18n.py +++ b/pyramid/i18n.py @@ -331,9 +331,9 @@ class Translations(gettext.GNUTranslations, object): """Like ``ugettext()``, but look the message up in the specified domain. """ - if PY3: # pragma: no cover + if PY3: return self._domains.get(domain, self).gettext(message) - else: # pragma: no cover + else: return self._domains.get(domain, self).ugettext(message) def dngettext(self, domain, singular, plural, num): @@ -352,10 +352,10 @@ class Translations(gettext.GNUTranslations, object): """Like ``ungettext()`` but look the message up in the specified domain. """ - if PY3: # pragma: no cover + if PY3: return self._domains.get(domain, self).ngettext( singular, plural, num) - else: # pragma: no cover + else: return self._domains.get(domain, self).ungettext( singular, plural, num) diff --git a/pyramid/scaffolds/tests.py b/pyramid/scaffolds/tests.py index dfbf9b6cf..db828759e 100644 --- a/pyramid/scaffolds/tests.py +++ b/pyramid/scaffolds/tests.py @@ -6,9 +6,9 @@ import tempfile import time try: + import http.client as httplib +except ImportError: import httplib -except ImportError: # pragma: no cover - import http.client as httplib #py3 class TemplateTest(object): def make_venv(self, directory): # pragma: no cover diff --git a/pyramid/tests/test_config/test_adapters.py b/pyramid/tests/test_config/test_adapters.py index 4cbb1bf80..b3b7576a3 100644 --- a/pyramid/tests/test_config/test_adapters.py +++ b/pyramid/tests/test_config/test_adapters.py @@ -219,7 +219,7 @@ class AdaptersConfiguratorMixinTests(unittest.TestCase): def test_add_response_adapter_dottednames(self): from pyramid.interfaces import IResponse config = self._makeOne(autocommit=True) - if PY3: # pragma: no cover + if PY3: str_name = 'builtins.str' else: str_name = '__builtin__.str' diff --git a/pyramid/tests/test_path.py b/pyramid/tests/test_path.py index fd927996a..f85373fd9 100644 --- a/pyramid/tests/test_path.py +++ b/pyramid/tests/test_path.py @@ -376,7 +376,7 @@ class TestDottedNameResolver(unittest.TestCase): def test_zope_dottedname_style_resolve_builtin(self): typ = self._makeOne() - if PY3: # pragma: no cover + if PY3: result = typ._zope_dottedname_style('builtins.str', None) else: result = typ._zope_dottedname_style('__builtin__.str', None) diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 48af98f59..5ae0b80b7 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -310,7 +310,7 @@ class TestRequest(unittest.TestCase): b'/\xe6\xb5\x81\xe8\xa1\x8c\xe8\xb6\x8b\xe5\x8a\xbf', 'utf-8' ) - if PY3: # pragma: no cover + if PY3: body = bytes(json.dumps({'a':inp}), 'utf-16') else: body = json.dumps({'a':inp}).decode('utf-8').encode('utf-16') diff --git a/pyramid/tests/test_scripts/test_pserve.py b/pyramid/tests/test_scripts/test_pserve.py index 107ff4c0a..75d4f5bef 100644 --- a/pyramid/tests/test_scripts/test_pserve.py +++ b/pyramid/tests/test_scripts/test_pserve.py @@ -4,7 +4,7 @@ import tempfile import unittest from pyramid.compat import PY3 -if PY3: # pragma: no cover +if PY3: import builtins as __builtin__ else: import __builtin__ diff --git a/pyramid/tests/test_traversal.py b/pyramid/tests/test_traversal.py index 0dcc4a027..aa3f1ad16 100644 --- a/pyramid/tests/test_traversal.py +++ b/pyramid/tests/test_traversal.py @@ -335,7 +335,7 @@ class ResourceTreeTraverserTests(unittest.TestCase): foo = DummyContext(bar, path) root = DummyContext(foo, 'root') policy = self._makeOne(root) - if PY3: # pragma: no cover + if PY3: vhm_root = b'/Qu\xc3\xa9bec'.decode('latin-1') else: vhm_root = b'/Qu\xc3\xa9bec' diff --git a/pyramid/tests/test_urldispatch.py b/pyramid/tests/test_urldispatch.py index 1755d9f47..20a3a4fc8 100644 --- a/pyramid/tests/test_urldispatch.py +++ b/pyramid/tests/test_urldispatch.py @@ -120,7 +120,7 @@ class RoutesMapperTests(unittest.TestCase): def test___call__pathinfo_cant_be_decoded(self): from pyramid.exceptions import URLDecodeError mapper = self._makeOne() - if PY3: # pragma: no cover + if PY3: path_info = b'\xff\xfe\xe6\x00'.decode('latin-1') else: path_info = b'\xff\xfe\xe6\x00' diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index a18fa8d16..292dfa024 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -284,9 +284,9 @@ class Test_object_description(unittest.TestCase): self.assertEqual(self._callFUT(('a', 'b')), "('a', 'b')") def test_set(self): - if PY3: # pragma: no cover + if PY3: self.assertEqual(self._callFUT(set(['a'])), "{'a'}") - else: # pragma: no cover + else: self.assertEqual(self._callFUT(set(['a'])), "set(['a'])") def test_list(self): diff --git a/pyramid/traversal.py b/pyramid/traversal.py index 4c275c4c1..a38cf271e 100644 --- a/pyramid/traversal.py +++ b/pyramid/traversal.py @@ -575,7 +575,7 @@ the ``safe`` argument to this function. This corresponds to the """ -if PY3: # pragma: no cover +if PY3: # special-case on Python 2 for speed? unchecked def quote_path_segment(segment, safe=''): """ %s """ % quote_path_segment_doc diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py index fe4d433c3..c40bec526 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -201,7 +201,7 @@ def _compile_route(route): def generator(dict): newdict = {} for k, v in dict.items(): - if PY3: # pragma: no cover + if PY3: if v.__class__ is binary_type: # url_quote below needs a native string, not bytes on Py3 v = v.decode('utf-8') diff --git a/pyramid/util.py b/pyramid/util.py index 6de53d559..abc7b2c88 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -291,7 +291,7 @@ def object_description(object): if isinstance(object, (bool, float, type(None))): return text_(str(object)) if isinstance(object, set): - if PY3: # pragma: no cover + if PY3: return shortrepr(object, '}') else: return shortrepr(object, ')') diff --git a/setup.cfg b/setup.cfg index a877ffb7f..bc092a6ca 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,6 @@ match=^test where=pyramid nocapture=1 cover-package=pyramid -cover-erase=1 [aliases] dev = develop easy_install pyramid[testing] diff --git a/tox.ini b/tox.ini index 3f32dbc3f..ba0007d7a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,17 @@ [tox] envlist = - py26,py27,py32,py33,py34,pypy,pypy3,cover + clean,py26,py27,py32,py33,py34,pypy,pypy3,report + +[testenv:clean] +commands = coverage erase +deps = coverage [testenv] commands = python setup.py dev - python setup.py test -q + python setup.py nosetests --with-coverage -[testenv:cover] -basepython = - python2.6 +[testenv:report] commands = python setup.py dev python setup.py nosetests --with-xunit --with-xcoverage --cover-min-percentage=100 -- cgit v1.2.3 From 149d36342e9970295ee49b0dfca7f1c896bce2a9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 19:02:36 -0600 Subject: update changelog --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index ea3323aa0..d6264b892 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -6,6 +6,9 @@ Features - Added support / testing for 'pypy3' under Tox and Travis. +- Automate code coverage metrics across py2 and py3 instead of just py2. + See https://github.com/Pylons/pyramid/pull/1471 + - Cache busting for static resources has been added and is available via a new argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``. See https://github.com/Pylons/pyramid/pull/1380 -- cgit v1.2.3 From 782eb470cf4b31c2cab75f3cc14a5f9c42eeb9f0 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 19:03:44 -0600 Subject: update changelog for #1469 --- CHANGES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.txt b/CHANGES.txt index ea3323aa0..46c331268 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -5,6 +5,7 @@ Features -------- - Added support / testing for 'pypy3' under Tox and Travis. + See https://github.com/Pylons/pyramid/pull/1469 - Cache busting for static resources has been added and is available via a new argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``. -- cgit v1.2.3 From 11ba5a50ef1c3c8eba4d58d0af0333f6bfe5ae61 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 21:34:17 -0600 Subject: test to see if files are shared between builds --- .travis.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 482d2a910..e2f379802 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,16 @@ # Wire up travis language: python - -env: - - TOXENV=clean - - TOXENV=py26 - - TOXENV=py27 - - TOXENV=py32 - - TOXENV=py33 - - TOXENV=py34 - - TOXENV=pypy - - TOXENV=pypy3 - - TOXENV=report +# +#env: +# - TOXENV=clean +# - TOXENV=py26 +# - TOXENV=py27 +# - TOXENV=py32 +# - TOXENV=py33 +# - TOXENV=py34 +# - TOXENV=pypy +# - TOXENV=pypy3 +# - TOXENV=report install: - travis_retry pip install tox -- cgit v1.2.3 From 370862eb748f74dacee6b2bb1a5a2e35f865018a Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 22 Nov 2014 23:31:50 -0800 Subject: add request processing diagram to docs/narr/router.rst --- docs/_static/pyramid_router.svg | 3 +++ docs/narr/router.rst | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 docs/_static/pyramid_router.svg diff --git a/docs/_static/pyramid_router.svg b/docs/_static/pyramid_router.svg new file mode 100644 index 000000000..21bbcb532 --- /dev/null +++ b/docs/_static/pyramid_router.svg @@ -0,0 +1,3 @@ + + +2014-11-23 07:19ZRequest Processingno exceptionsmiddleware ingress tween ingresstraversalContextFoundtween egressresponse callbacksfinished callbacksmiddleware egressBeforeRenderRequest ProcessingLegendeventcallbackviewexternal process (middleware, tween)internal processview pipelinepredicatesview lookuproute predicatesURL dispatchNewRequestNewResponseview mapper ingressviewview mapper egressresponse adapterdecorators ingressdecorators egressauthorization diff --git a/docs/narr/router.rst b/docs/narr/router.rst index ac3deefdc..745c2faa1 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -9,6 +9,9 @@ Request Processing ================== +.. image:: ../_static/pyramid_router.svg + :alt: Request Processing + Once a :app:`Pyramid` application is up and running, it is ready to accept requests and return responses. What happens from the time a :term:`WSGI` request enters a :app:`Pyramid` application through to the point that -- cgit v1.2.3 From 39846565d0d98a6a9ef2ef34faad3eb620b3e9fb Mon Sep 17 00:00:00 2001 From: Hugo Branquinho Date: Tue, 25 Nov 2014 16:31:04 +0000 Subject: Shortcut for package name on registry --- pyramid/registry.py | 5 +++++ pyramid/tests/test_registry.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/pyramid/registry.py b/pyramid/registry.py index 606251a8d..8c05940b9 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -5,6 +5,7 @@ from zope.interface import implementer from zope.interface.registry import Components from pyramid.compat import text_ +from pyramid.decorator import reify from pyramid.interfaces import ( ISettings, @@ -42,6 +43,10 @@ class Registry(Components, dict): # defeat bool determination via dict.__len__ return True + @reify + def package_name(self): + return self.__name__ + def registerSubscriptionAdapter(self, *arg, **kw): result = Components.registerSubscriptionAdapter(self, *arg, **kw) self.has_listeners = True diff --git a/pyramid/tests/test_registry.py b/pyramid/tests/test_registry.py index 11019b852..50f49f24d 100644 --- a/pyramid/tests/test_registry.py +++ b/pyramid/tests/test_registry.py @@ -12,6 +12,11 @@ class TestRegistry(unittest.TestCase): registry = self._makeOne() self.assertEqual(registry.__nonzero__(), True) + def test_package_name(self): + package_name = 'testing' + registry = self._getTargetClass()(package_name) + self.assertEqual(registry.package_name, package_name) + def test_registerHandler_and_notify(self): registry = self._makeOne() self.assertEqual(registry.has_listeners, False) -- cgit v1.2.3 From d89c5f76b3032a1447f19dc87a7a6ceb7508c3cb Mon Sep 17 00:00:00 2001 From: Hugo Branquinho Date: Tue, 25 Nov 2014 19:38:24 +0000 Subject: Documentation added --- CONTRIBUTORS.txt | 2 ++ docs/api/registry.rst | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 66f029cb7..9c2191f3b 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -234,3 +234,5 @@ Contributors - Fenton Travers, 2014/05/06 - Randall Leeds, 2014/11/11 + +- Hugo Branquinho, 2014/11/25 diff --git a/docs/api/registry.rst b/docs/api/registry.rst index bab3e26ba..57a80b3f5 100644 --- a/docs/api/registry.rst +++ b/docs/api/registry.rst @@ -14,6 +14,18 @@ accessed as ``request.registry.settings`` or ``config.registry.settings`` in a typical Pyramid application. + .. attribute:: package_name + + .. versionadded:: 1.6 + + When a registry is set up (or created) by a :term:`Configurator`, this + attribute will be the shortcut for + :attr:`pyramid.config.Configurator.package_name`. + + This attribute is often accessed as ``request.registry.package_name`` or + ``config.registry.package_name`` or ``config.package_name`` + in a typical Pyramid application. + .. attribute:: introspector .. versionadded:: 1.3 -- cgit v1.2.3 From 138706a24bd8e7051c60942c2789d8c16b4ca2ed Mon Sep 17 00:00:00 2001 From: Matt Russell Date: Wed, 19 Nov 2014 23:06:17 +0000 Subject: Include code examples for integration and functional tests in docs #1001 Wrap lines as per convention. --- docs/narr/MyProject/myproject/tests.py | 37 +++++++++ docs/narr/MyProject/setup.py | 45 +++++++---- docs/narr/testing.rst | 142 +++++++++++++++------------------ 3 files changed, 131 insertions(+), 93 deletions(-) diff --git a/docs/narr/MyProject/myproject/tests.py b/docs/narr/MyProject/myproject/tests.py index 64dcab1d5..8c60407e5 100644 --- a/docs/narr/MyProject/myproject/tests.py +++ b/docs/narr/MyProject/myproject/tests.py @@ -15,3 +15,40 @@ class ViewTests(unittest.TestCase): request = testing.DummyRequest() info = my_view(request) self.assertEqual(info['project'], 'MyProject') + +class ViewIntegrationTests(unittest.TestCase): + def setUp(self): + """ This sets up the application registry with the + registrations your application declares in its ``includeme`` + function. + """ + self.config = testing.setUp() + self.config.include('myproject') + + def tearDown(self): + """ Clear out the application registry """ + testing.tearDown() + + def test_my_view(self): + from myproject.views import my_view + request = testing.DummyRequest() + result = my_view(request) + self.assertEqual(result.status, '200 OK') + body = result.app_iter[0] + self.assertTrue('Welcome to' in body) + self.assertEqual(len(result.headerlist), 2) + self.assertEqual(result.headerlist[0], + ('Content-Type', 'text/html; charset=UTF-8')) + self.assertEqual(result.headerlist[1], ('Content-Length', + str(len(body)))) + +class FunctionalTests(unittest.TestCase): + def setUp(self): + from myproject import main + app = main({}) + from webtest import TestApp + self.testapp = TestApp(app) + + def test_root(self): + res = self.testapp.get('/', status=200) + self.assertTrue('Pyramid' in res.body) diff --git a/docs/narr/MyProject/setup.py b/docs/narr/MyProject/setup.py index 8c019af51..9f34540a7 100644 --- a/docs/narr/MyProject/setup.py +++ b/docs/narr/MyProject/setup.py @@ -1,30 +1,42 @@ -import os +"""Setup for the MyProject package. +""" +import os from setuptools import setup, find_packages -here = os.path.abspath(os.path.dirname(__file__)) -with open(os.path.join(here, 'README.txt')) as f: - README = f.read() -with open(os.path.join(here, 'CHANGES.txt')) as f: - CHANGES = f.read() -requires = [ +HERE = os.path.abspath(os.path.dirname(__file__)) + + +with open(os.path.join(HERE, 'README.txt')) as fp: + README = fp.read() + + +with open(os.path.join(HERE, 'CHANGES.txt')) as fp: + CHANGES = fp.read() + + +REQUIRES = [ 'pyramid', 'pyramid_chameleon', 'pyramid_debugtoolbar', 'waitress', ] +TESTS_REQUIRE = [ + 'webtest' + ] + setup(name='MyProject', version='0.0', description='MyProject', long_description=README + '\n\n' + CHANGES, classifiers=[ - "Programming Language :: Python", - "Framework :: Pyramid", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - ], + 'Programming Language :: Python', + 'Framework :: Pyramid', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', + ], author='', author_email='', url='', @@ -32,11 +44,10 @@ setup(name='MyProject', packages=find_packages(), include_package_data=True, zip_safe=False, - install_requires=requires, - tests_require=requires, - test_suite="myproject", + install_requires=REQUIRES, + tests_require=TESTS_REQUIRE, + test_suite='myproject', entry_points="""\ [paste.app_factory] main = myproject:main - """, - ) + """) diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst index e001ad81c..3620f5e11 100644 --- a/docs/narr/testing.rst +++ b/docs/narr/testing.rst @@ -128,8 +128,9 @@ functions accepts various arguments that influence the environment of the test. See the :ref:`testing_module` API for information about the extra arguments supported by these functions. -If you also want to make :func:`~pyramid.threadlocal.get_current_request` return something -other than ``None`` during the course of a single test, you can pass a +If you also want to make :func:`~pyramid.threadlocal.get_current_request` +return something other than ``None`` during the course of a single test, you +can pass a :term:`request` object into the :func:`pyramid.testing.setUp` within the ``setUp`` method of your test: @@ -333,66 +334,49 @@ Creating Integration Tests -------------------------- In :app:`Pyramid`, a *unit test* typically relies on "mock" or "dummy" -implementations to give the code under test only enough context to run. +implementations to give the code under test enough context to run. "Integration testing" implies another sort of testing. In the context of a -:app:`Pyramid` integration test, the test logic tests the functionality of -some code *and* its integration with the rest of the :app:`Pyramid` +:app:`Pyramid` integration test, the test logic exercises the functionality of +the code under test *and* its integration with the rest of the :app:`Pyramid` framework. -In :app:`Pyramid` applications that are plugins to Pyramid, you can create an -integration test by including its ``includeme`` function via -:meth:`pyramid.config.Configurator.include` in the test's setup code. This -causes the entire :app:`Pyramid` environment to be set up and torn down as if -your application was running "for real". This is a heavy-hammer way of -making sure that your tests have enough context to run properly, and it tests -your code's integration with the rest of :app:`Pyramid`. +Creating an integration test for a :app:`Pyramid` application usually means +invoking the application's ``includeme`` function via +:meth:`pyramid.config.Configurator.include` within the test's setup code. This +causes the entire :app:`Pyramid` environment to be set up, simulating what +happens when your application is run "for real". This is a heavy-hammer way of +making sure that your tests have enough context to run properly, and tests your +code's integration with the rest of :app:`Pyramid`. -Let's demonstrate this by showing an integration test for a view. The below -test assumes that your application's package name is ``myapp``, and that -there is a ``views`` module in the app with a function with the name -``my_view`` in it that returns the response 'Welcome to this application' -after accessing some values that require a fully set up environment. +.. seealso:: -.. code-block:: python - :linenos: + See more information about :app:`Pyramid`'s ``includme`` function. - import unittest +Let's demonstrate this by showing an integration test for a view. - from pyramid import testing +Given the following view definition, which assumes that your application's +:term:`package` name is ``myproject``, and within that :term:`package` there +exists a module ``views``, which in turn contains a :term:`view` function named +``my_view``: - class ViewIntegrationTests(unittest.TestCase): - def setUp(self): - """ This sets up the application registry with the - registrations your application declares in its ``includeme`` - function. - """ - import myapp - self.config = testing.setUp() - self.config.include('myapp') + .. literalinclude:: MyProject/myproject/views.py + :linenos: + :lines: 1-6 + :language: python - def tearDown(self): - """ Clear out the application registry """ - testing.tearDown() +You'd then create a ``tests`` module within your ``myproject`` package, +containing the following test code: - def test_my_view(self): - from myapp.views import my_view - request = testing.DummyRequest() - result = my_view(request) - self.assertEqual(result.status, '200 OK') - body = result.app_iter[0] - self.assertTrue('Welcome to' in body) - self.assertEqual(len(result.headerlist), 2) - self.assertEqual(result.headerlist[0], - ('Content-Type', 'text/html; charset=UTF-8')) - self.assertEqual(result.headerlist[1], ('Content-Length', - str(len(body)))) - -Unless you cannot avoid it, you should prefer writing unit tests that use the -:class:`~pyramid.config.Configurator` API to set up the right "mock" -registrations rather than creating an integration test. Unit tests will run -faster (because they do less for each test) and the result of a unit test is -usually easier to make assertions about. + .. literalinclude:: MyProject/myproject/tests.py + :linenos: + :pyobject: ViewIntegrationTests + :language: python + +Writing unit tests that use the :class:`~pyramid.config.Configurator` API to +set up the right "mock" registrations is often preferred to creating +integration tests. Unit tests will run faster (because they do less for each +test) and are usually easier to reason about. .. index:: single: functional tests @@ -404,34 +388,40 @@ Creating Functional Tests Functional tests test your literal application. -The below test assumes that your application's package name is ``myapp``, and -that there is a view that returns an HTML body when the root URL is invoked. -It further assumes that you've added a ``tests_require`` dependency on the -``WebTest`` package within your ``setup.py`` file. :term:`WebTest` is a -functional testing package written by Ian Bicking. +In Pyramid, functional tests are typically written using the :term:`WebTest` +package, which provides APIs for invoking HTTP(S) requests to your application. -.. code-block:: python - :linenos: +Regardless of which testing :term:`package` you use, ensure to add a +``tests_require`` dependency on that package to to your application's +``setup.py`` file: - import unittest + .. literalinclude:: MyProject/setup.py + :linenos: + :emphasize-lines: 26-28,48 + :language: python - class FunctionalTests(unittest.TestCase): - def setUp(self): - from myapp import main - app = main({}) - from webtest import TestApp - self.testapp = TestApp(app) - - def test_root(self): - res = self.testapp.get('/', status=200) - self.assertTrue('Pyramid' in res.body) - -When this test is run, each test creates a "real" WSGI application using the -``main`` function in your ``myapp.__init__`` module and uses :term:`WebTest` -to wrap that WSGI application. It assigns the result to ``self.testapp``. -In the test named ``test_root``, we use the testapp's ``get`` method to -invoke the root URL. We then assert that the returned HTML has the string -``Pyramid`` in it. +Assuming your :term:`package` is named ``myproject``, which contains a +``views`` module, which in turn contains a :term:`view` function ``my_view`` +that returns a HTML body when the root URL is invoked: + + .. literalinclude:: MyProject/myproject/views.py + :linenos: + :language: python + +Then the following example functional test (shown below) demonstrates invoking +the :term:`view` shown above: + + .. literalinclude:: MyProject/myproject/tests.py + :linenos: + :pyobject: FunctionalTests + :language: python + +When this test is run, each test method creates a "real" :term:`WSGI` +application using the ``main`` function in your ``myproject.__init__`` module, +using :term:`WebTest` to wrap that WSGI application. It assigns the result to +``self.testapp``. In the test named ``test_root``. The ``TestApp``'s ``get`` +method is used to invoke the root URL. Finally, an assertion is made that the +returned HTML contains the text ``MyProject``. See the :term:`WebTest` documentation for further information about the methods available to a :class:`webtest.app.TestApp` instance. -- cgit v1.2.3 From 9c94e129f1bbb753317deba7ea5f790db13e0709 Mon Sep 17 00:00:00 2001 From: Matt Russell Date: Tue, 25 Nov 2014 20:59:18 +0000 Subject: Tweak seealso for the includeme function. --- docs/narr/testing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst index 3620f5e11..ecda57489 100644 --- a/docs/narr/testing.rst +++ b/docs/narr/testing.rst @@ -351,7 +351,7 @@ code's integration with the rest of :app:`Pyramid`. .. seealso:: - See more information about :app:`Pyramid`'s ``includme`` function. + See also :ref:`including_configuration` Let's demonstrate this by showing an integration test for a view. -- cgit v1.2.3 From bc56b88f9d306e510044dc9fede0d36d8e88d8eb Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 26 Nov 2014 15:05:50 -0500 Subject: Unused import. --- pyramid/authentication.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyramid/authentication.py b/pyramid/authentication.py index f4c211ffa..aefa180b5 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -3,7 +3,6 @@ from codecs import utf_8_decode from codecs import utf_8_encode import hashlib import base64 -import datetime import re import time as time_mod import warnings -- cgit v1.2.3 From ea58f249f89bb9f48b926baf1e4fcc04832db672 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 26 Nov 2014 15:06:02 -0500 Subject: 79 columns. --- pyramid/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/authentication.py b/pyramid/authentication.py index aefa180b5..09c8a2d3a 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -928,7 +928,7 @@ class AuthTktCookieHelper(object): if reissue and not hasattr(request, '_authtkt_reissued'): if ( (now - timestamp) > self.reissue_time ): - # work around https://github.com/Pylons/pyramid/issues#issue/108 + # See https://github.com/Pylons/pyramid/issues#issue/108 tokens = list(filter(None, tokens)) headers = self.remember(request, userid, max_age=self.max_age, tokens=tokens) -- cgit v1.2.3 From ec5226745f8f5161f89636e036e2b8efed216b74 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 27 Nov 2014 10:49:33 -0600 Subject: fix issue in auth_tkt parsing with the cookie type being unicode In webob the cookies are always unicode but the auth_tkt tests were expecting them to be a native string. This didn't manifest itself until we started using the ``hmac.compare_digest`` which fails if the types are not the same. Fixes #1477 --- pyramid/authentication.py | 2 +- pyramid/tests/test_authentication.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 09c8a2d3a..e0e241e52 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -740,7 +740,7 @@ def parse_ticket(secret, ticket, ip, hashalg='md5'): If the ticket cannot be parsed, a ``BadTicket`` exception will be raised with an explanation. """ - ticket = ticket.strip('"') + ticket = native_(ticket).strip('"') digest_size = hashlib.new(hashalg).digest_size * 2 digest = ticket[:digest_size] try: diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index e25e9faa1..920a7e65d 100644 --- a/pyramid/tests/test_authentication.py +++ b/pyramid/tests/test_authentication.py @@ -1211,26 +1211,26 @@ class Test_parse_ticket(unittest.TestCase): self._assertRaisesBadTicket('secret', ticket, '0.0.0.0') def test_correct_with_user_data(self): - ticket = '66f9cc3e423dc57c91df696cf3d1f0d80000000auserid!a,b!' + ticket = u'66f9cc3e423dc57c91df696cf3d1f0d80000000auserid!a,b!' result = self._callFUT('secret', ticket, '0.0.0.0') self.assertEqual(result, (10, 'userid', ['a', 'b'], '')) def test_correct_with_user_data_sha512(self): - ticket = '7d947cdef99bad55f8e3382a8bd089bb9dd0547f7925b7d189adc1160cab'\ - '0ec0e6888faa41eba641a18522b26f19109f3ffafb769767ba8a26d02aae'\ - 'ae56599a0000000auserid!a,b!' + ticket = u'7d947cdef99bad55f8e3382a8bd089bb9dd0547f7925b7d189adc1160ca'\ + 'b0ec0e6888faa41eba641a18522b26f19109f3ffafb769767ba8a26d02aa'\ + 'eae56599a0000000auserid!a,b!' result = self._callFUT('secret', ticket, '0.0.0.0', 'sha512') self.assertEqual(result, (10, 'userid', ['a', 'b'], '')) def test_ipv4(self): - ticket = 'b3e7156db4f8abde4439c4a6499a0668f9e7ffd7fa27b798400ecdade8d7'\ - '6c530000000auserid!' + ticket = u'b3e7156db4f8abde4439c4a6499a0668f9e7ffd7fa27b798400ecdade8d'\ + '76c530000000auserid!' result = self._callFUT('secret', ticket, '198.51.100.1', 'sha256') self.assertEqual(result, (10, 'userid', [''], '')) def test_ipv6(self): - ticket = 'd025b601a0f12ca6d008aa35ff3a22b7d8f3d1c1456c85becf8760cd7a2f'\ - 'a4910000000auserid!' + ticket = u'd025b601a0f12ca6d008aa35ff3a22b7d8f3d1c1456c85becf8760cd7a2'\ + 'fa4910000000auserid!' result = self._callFUT('secret', ticket, '2001:db8::1', 'sha256') self.assertEqual(result, (10, 'userid', [''], '')) pass -- cgit v1.2.3 From eb2bc87f663d2e056c57a9bdbb233cf574a5a1a9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 29 Nov 2014 11:45:46 -0600 Subject: fix build on py3.2 missing u-prefix introduced in ec5226745f8f5161f89636e036e2b8efed216b74 --- pyramid/tests/test_authentication.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index 920a7e65d..c7fc1c211 100644 --- a/pyramid/tests/test_authentication.py +++ b/pyramid/tests/test_authentication.py @@ -1211,26 +1211,26 @@ class Test_parse_ticket(unittest.TestCase): self._assertRaisesBadTicket('secret', ticket, '0.0.0.0') def test_correct_with_user_data(self): - ticket = u'66f9cc3e423dc57c91df696cf3d1f0d80000000auserid!a,b!' + ticket = text_('66f9cc3e423dc57c91df696cf3d1f0d80000000auserid!a,b!') result = self._callFUT('secret', ticket, '0.0.0.0') self.assertEqual(result, (10, 'userid', ['a', 'b'], '')) def test_correct_with_user_data_sha512(self): - ticket = u'7d947cdef99bad55f8e3382a8bd089bb9dd0547f7925b7d189adc1160ca'\ - 'b0ec0e6888faa41eba641a18522b26f19109f3ffafb769767ba8a26d02aa'\ - 'eae56599a0000000auserid!a,b!' + ticket = text_('7d947cdef99bad55f8e3382a8bd089bb9dd0547f7925b7d189adc1' + '160cab0ec0e6888faa41eba641a18522b26f19109f3ffafb769767' + 'ba8a26d02aaeae56599a0000000auserid!a,b!') result = self._callFUT('secret', ticket, '0.0.0.0', 'sha512') self.assertEqual(result, (10, 'userid', ['a', 'b'], '')) def test_ipv4(self): - ticket = u'b3e7156db4f8abde4439c4a6499a0668f9e7ffd7fa27b798400ecdade8d'\ - '76c530000000auserid!' + ticket = text_('b3e7156db4f8abde4439c4a6499a0668f9e7ffd7fa27b798400ecd' + 'ade8d76c530000000auserid!') result = self._callFUT('secret', ticket, '198.51.100.1', 'sha256') self.assertEqual(result, (10, 'userid', [''], '')) def test_ipv6(self): - ticket = u'd025b601a0f12ca6d008aa35ff3a22b7d8f3d1c1456c85becf8760cd7a2'\ - 'fa4910000000auserid!' + ticket = text_('d025b601a0f12ca6d008aa35ff3a22b7d8f3d1c1456c85becf8760' + 'cd7a2fa4910000000auserid!') result = self._callFUT('secret', ticket, '2001:db8::1', 'sha256') self.assertEqual(result, (10, 'userid', [''], '')) pass -- cgit v1.2.3 From 3408269bd291b771efef8e54f039038fc5b59a26 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 1 Dec 2014 13:40:39 -0800 Subject: - rename pyramid_router.svg to pyramid_request_processing.svg to be consistent with its content - add source files for future modifications --- docs/_static/pyramid_request_processing.graffle | 9748 +++++++++++++++++++++++ docs/_static/pyramid_request_processing.png | Bin 0 -> 122854 bytes docs/_static/pyramid_request_processing.svg | 3 + docs/_static/pyramid_router.svg | 3 - docs/narr/router.rst | 2 +- 5 files changed, 9752 insertions(+), 4 deletions(-) create mode 100644 docs/_static/pyramid_request_processing.graffle create mode 100644 docs/_static/pyramid_request_processing.png create mode 100644 docs/_static/pyramid_request_processing.svg delete mode 100644 docs/_static/pyramid_router.svg diff --git a/docs/_static/pyramid_request_processing.graffle b/docs/_static/pyramid_request_processing.graffle new file mode 100644 index 000000000..71319610b --- /dev/null +++ b/docs/_static/pyramid_request_processing.graffle @@ -0,0 +1,9748 @@ + + + + + ActiveLayerIndex + 0 + ApplicationVersion + + com.omnigroup.OmniGrafflePro + 139.18.0.187838 + + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {576, 733}} + Class + SolidGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + BaseZoom + 0 + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + CreationDate + 2014-11-18 08:33:33 +0000 + Creator + Steve Piercy + DisplayScale + 1 0/72 in = 1 0/72 in + GraphDocumentVersion + 8 + GraphicsList + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169389 + + ID + 169504 + Layer + 0 + Points + + {344.41667175292969, 402.88506673894034} + {375.5, 402.27232108797347} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169428 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169382 + + ID + 169433 + Layer + 0 + Points + + {155.00000254313238, 459.27667544230695} + {238.5002713470962, 456.52468399152298} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169370 + Position + 0.28820157051086426 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169383 + + ID + 169432 + Layer + 0 + Points + + {155.00000254313238, 482.12574895537085} + {238.52297468463752, 508.35839132916635} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169370 + Position + 0.5668826699256897 + + + + Class + Group + Graphics + + + Bounds + {{238.8333613077798, 284.99999999999994}, {105.66668701171875, 18.656048080136394}} + Class + ShapedGraphic + ID + 169425 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, -0.49999999999999645} + {-0.49526813868737474, -0.4689979626999552} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 authorization} + VerticalPad + 0 + + + + Bounds + {{238.75000762939453, 412.15071036499205}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169426 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, 0.5} + {-0.49999999999999911, 0.49999999999999289} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 decorators egress} + VerticalPad + 0 + + + + Bounds + {{238.75000762939453, 303.65604172230951}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169427 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, -0.49999999999999645} + {-0.49526813868737474, -0.4689979626999552} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 decorators ingress} + VerticalPad + 0 + + + + Bounds + {{238.75000762939453, 393.55704269887212}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169428 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 response adapter} + VerticalPad + 0 + + + + Bounds + {{238.75000762939453, 374.90099016834085}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169429 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view mapper egress} + VerticalPad + 0 + + + + Bounds + {{238.75000762939453, 341.36561209044055}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169430 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view} + VerticalPad + 0 + + + + Bounds + {{238.75000762939453, 322.26348241170439}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169431 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view mapper ingress} + VerticalPad + 0 + + + + ID + 169424 + Layer + 0 + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169422 + Info + 4 + + ID + 169423 + Layer + 0 + Points + + {155.00000254313238, 470.25295298442387} + {238.33861159880226, 482.4262543949045} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169370 + Position + 0.42701038718223572 + + + + Bounds + {{238.83336130777977, 471.22620192028251}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169422 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 NewResponse} + VerticalPad + 0 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169420 + Info + 4 + + ID + 169421 + Layer + 0 + Points + + {154.99998733539806, 128.68025330008533} + {239.83340199788393, 128.59152244387357} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169386 + Position + 0.35945424437522888 + + + + Bounds + {{239.83340199788395, 117.31920169649808}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169420 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 NewRequest} + VerticalPad + 0 + + + + Class + TableGroup + Graphics + + + Bounds + {{102.1666056315114, 148.28868579864499}, {105.66669464111328, 33.08929443359375}} + Class + ShapedGraphic + ID + 169418 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 URL dispatch} + VerticalPad + 0 + + + + Bounds + {{102.1666056315114, 181.37798023223874}, {105.66669464111328, 17.244049072265625}} + Class + ShapedGraphic + ID + 169419 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 route predicates} + VerticalPad + 0 + + + + GridH + + 169418 + 169419 + + + ID + 169417 + Layer + 0 + + + Class + TableGroup + Graphics + + + Bounds + {{102.16666158040482, 272}, {105.66666412353516, 33.08929443359375}} + Class + ShapedGraphic + ID + 169412 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view lookup} + VerticalPad + 0 + + + + Bounds + {{102.16666158040482, 305.08929443359375}, {105.66666412353516, 17.244049072265625}} + Class + ShapedGraphic + ID + 169413 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 predicates} + VerticalPad + 0 + + + + GridH + + 169412 + 169413 + + + ID + 169411 + Layer + 0 + + + Class + LineGraphic + Head + + ID + 169407 + Info + 7 + + ID + 169410 + Layer + 0 + Points + + {238.75000762939462, 430.80675844512831} + {207.66666666666765, 385.656005859375} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169426 + Info + 6 + + + + Class + LineGraphic + Head + + ID + 169407 + Info + 8 + + ID + 169409 + Layer + 0 + Points + + {239.33336141608385, 285.57837549845181} + {207.66666666666777, 353.07514659563753} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169425 + Info + 6 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -8.9999999999999432} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169381 + + ID + 169408 + Layer + 0 + Points + + {155.00000254313238, 386.66442959065108} + {155.00000254313238, 422.21209462483216} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169407 + + + + Bounds + {{102.16667048136482, 353.07514659563753}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169407 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {-0.49211360058019871, -0.49251945318722434} + {-0.49211360058019871, 0.49470854679786669} + {0.4984227008620481, 0.48463479169597612} + {0.49842270086204898, -0.5} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view pipeline} + VerticalPad + 0 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169380 + Info + 4 + + ID + 169399 + Layer + 0 + Points + + {154.9999936421724, 258.44082431579938} + {238.8333613077798, 258.45536063967575} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169372 + Position + 0.51973581314086914 + + + + Class + Group + Graphics + + + Bounds + {{383.66662216186666, 130.51770718892479}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169393 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 internal process} + VerticalPad + 0 + + + + Bounds + {{383.66662216186666, 91.940789540609359}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169394 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 external process (middleware, tween)} + VerticalPad + 0 + + + + Bounds + {{383.66662216186666, 158.54998334248924}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169395 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view} + VerticalPad + 0 + + + + Bounds + {{383.66662216186666, 186.58225949605369}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169396 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 callback} + VerticalPad + 0 + + + + Bounds + {{383.66662216186666, 63.908513387045019}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169397 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 event} + VerticalPad + 0 + + + + Bounds + {{370.9999504089372, 42.910746256511771}, {132.66667175292969, 184.08924865722656}} + Class + ShapedGraphic + ID + 169398 + Magnets + + {1, 0.5} + {1, -0.5} + {-1, 0.5} + {-1, -0.5} + {0.5, 1} + {-0.5, 1} + {0.5, -1} + {-0.5, -1} + + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + CornerRadius + 5 + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\b\fs20 \cf0 Legend} + VerticalPad + 0 + + TextPlacement + 0 + + + ID + 169391 + Layer + 0 + + + Bounds + {{233.5000012715667, 20.000000000000934}, {116, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + FontInfo + + Font + Helvetica + Size + 12 + + ID + 169390 + Layer + 0 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\b\fs24 \cf0 <%Canvas%>} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{375.5, 391}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169389 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 BeforeRender} + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {0, 7.05596923828125} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169418 + Info + 2 + + ID + 169386 + Layer + 0 + Points + + {155.00000170434049, 119.22767858295661} + {154.99995295206804, 148.28868579864499} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169378 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169378 + + ID + 169385 + Layer + 0 + Points + + {155.00000254313238, 67.727678571434836} + {155.00000254313238, 96.18303707668386} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169377 + Info + 1 + + + + Bounds + {{102.16667048136482, 509.6179466247504}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169384 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 middleware egress} + VerticalPad + 0 + + + + Bounds + {{239, 497.23589324949899}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169383 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 finished callbacks} + VerticalPad + 0 + + + + Bounds + {{239, 445.23589324949717}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169382 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 response callbacks} + VerticalPad + 0 + + + + Bounds + {{102.16667048136482, 422.21209462483216}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169381 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 tween egress} + VerticalPad + 0 + + + + Bounds + {{238.83336130777977, 247.18303989230026}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169380 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 ContextFound} + VerticalPad + 0 + + + + Bounds + {{102.16667048136482, 222.18303707668389}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169379 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 traversal} + VerticalPad + 0 + + + + Bounds + {{102.16667048136482, 96.18303707668386}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169378 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 tween ingress} + VerticalPad + 0 + + + + Bounds + {{102.16667048136482, 45.18303707668386}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169377 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 middleware ingress } + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169379 + + ID + 169373 + Layer + 0 + Points + + {154.99995295206804, 198.62202930450437} + {155.00000254313238, 222.18303707668389} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169419 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {0, 7.05596923828125} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169412 + Info + 2 + + ID + 169372 + Layer + 0 + Points + + {154.9999936421724, 245.22767856643924} + {154.9999936421724, 272} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169379 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -8.9999999999999432} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169407 + + ID + 169371 + Layer + 0 + Points + + {154.9999936421724, 322.33334350585938} + {155.00000254313238, 353.07514659563753} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169413 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9839935302734375} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169384 + Info + 2 + + ID + 169370 + Layer + 0 + Points + + {155.00000254313238, 444.75673611958314} + {155.00000254313238, 509.6179466247504} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169381 + + + + Class + LineGraphic + Head + + ID + 169444 + Info + 6 + + ID + 169503 + Layer + 1 + Points + + {272.4166717529298, 537.32234122436705} + {420.4999504089364, 515.08928491955714} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169494 + Info + 5 + + + + Class + LineGraphic + Head + + ID + 169444 + + ID + 169502 + Layer + 1 + Points + + {272.50004831949906, 391.51558277923863} + {420.4999504089364, 472.78869058972316} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169493 + Info + 5 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169450 + + ID + 169501 + Layer + 1 + Points + + {83.000002543132396, 592.81693102013151} + {239, 583.78422005970799} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169438 + Position + 0.28820157051086426 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169451 + + ID + 169500 + Layer + 1 + Points + + {83.000002543132396, 629.80996162681686} + {239, 640.78422005970981} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169438 + Position + 0.5668826699256897 + + + + Class + Group + Graphics + + + Bounds + {{166.8333613077798, 391.51558277923863}, {105.66668701171875, 18.656048080136394}} + Class + ShapedGraphic + ID + 169493 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, -0.49999999999999645} + {-0.49526813868737474, -0.4689979626999552} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 authorization} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 518.66629314423074}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169494 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, 0.5} + {-0.49999999999999911, 0.49999999999999289} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 decorators egress} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 410.17162450154819}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169495 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, -0.49999999999999645} + {-0.49526813868737474, -0.4689979626999552} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 decorators ingress} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 500.07262547811081}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169496 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 response adapter} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 481.41657294757954}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169497 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view mapper egress} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 447.88119486967923}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169498 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 428.77906519094307}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169499 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view mapper ingress} + VerticalPad + 0 + + + + ID + 169492 + Layer + 1 + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169490 + Info + 4 + + ID + 169491 + Layer + 1 + Points + + {83.166643778483959, 611.77452873049333} + {238.8333613077798, 611.77452873049333} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + + + Bounds + {{238.83336130777977, 600.50220798311784}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169490 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 NewResponse} + VerticalPad + 0 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169488 + Info + 4 + + ID + 169489 + Layer + 1 + Points + + {82.999986314263907, 140.3328574622312} + {239.83340199788393, 141.59152244387357} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169454 + Position + 0.35945424437522888 + + + + Bounds + {{239.83340199788395, 130.31920169649808}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169488 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 NewRequest} + VerticalPad + 0 + + + + Class + TableGroup + Graphics + + + Bounds + {{30.166605631511416, 166.28868579864499}, {105.66668701171875, 33.08929443359375}} + Class + ShapedGraphic + ID + 169486 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 URL dispatch} + VerticalPad + 0 + + + + Bounds + {{30.166605631511416, 199.37798023223874}, {105.66668701171875, 17.244049072265625}} + Class + ShapedGraphic + ID + 169487 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 route predicates} + VerticalPad + 0 + + + + GridH + + 169486 + 169487 + + + ID + 169485 + Layer + 1 + + + Class + TableGroup + Graphics + + + Bounds + {{420.5000406901047, 338.15028762817326}, {105.66668701171875, 33.08929443359375}} + Class + ShapedGraphic + ID + 169483 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view lookup} + VerticalPad + 0 + + + + Bounds + {{420.5000406901047, 371.23958206176701}, {105.66668701171875, 17.244049072265625}} + Class + ShapedGraphic + ID + 169484 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 predicates} + VerticalPad + 0 + + + + GridH + + 169483 + 169484 + + + ID + 169482 + Layer + 1 + + + Class + TableGroup + Graphics + + + Bounds + {{30.166661580404835, 335}, {105.66667175292969, 33.08929443359375}} + Class + ShapedGraphic + ID + 169480 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view lookup} + VerticalPad + 0 + + + + Bounds + {{30.166661580404835, 368.08929443359375}, {105.66667175292969, 17.244049072265625}} + Class + ShapedGraphic + ID + 169481 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 predicates} + VerticalPad + 0 + + + + GridH + + 169480 + 169481 + + + ID + 169479 + Layer + 1 + + + Class + LineGraphic + Head + + ID + 169475 + Info + 7 + + ID + 169478 + Layer + 1 + Points + + {166.75000762939462, 537.32234122436694} + {135.66666666666765, 485} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169494 + Info + 6 + + + + Class + LineGraphic + Head + + ID + 169475 + Info + 8 + + ID + 169477 + Layer + 1 + Points + + {167.33336141608385, 392.09395827769049} + {135.66666666666777, 452.41914073626253} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169493 + Info + 6 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -8.9999999999999432} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169449 + + ID + 169476 + Layer + 1 + Points + + {83.000002543132396, 485.50842372576449} + {83.000002543132396, 548.10604731241608} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169475 + + + + Bounds + {{30.166670481364818, 452.41914073626253}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169475 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {-0.49211360058019871, -0.49251945318722434} + {-0.49211360058019871, 0.49470854679786669} + {0.4984227008620481, 0.48463479169597612} + {0.49842270086204898, -0.5} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view pipeline} + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {51.333333333333314, 0} + {-0.66666666666662877, 58.666666666666686} + {0.66673293066804717, -58.666850540458825} + {-16.306719354194399, 0.26652623861849634} + + Head + + ID + 169443 + Info + 4 + + ID + 169474 + Layer + 1 + Points + + {369.66666666666669, 541} + {404.00000000000023, 362} + {420.36749776329049, 302.42112495959606} + + Style + + stroke + + Bezier + + Color + + b + 0.75663 + g + 0.756618 + r + 0.75664 + + HeadArrow + 0 + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + + + Class + LineGraphic + ControlPoints + + {69.833333333332462, -0.72767857143483639} + {-0.66690523835279691, -51.044605218028948} + {0.66666666666674246, 51.044637362162291} + {-24.333271383961971, -0.13425428344572765} + + Head + + ID + 169443 + Info + 4 + + ID + 169473 + Layer + 1 + Points + + {310.66666666666754, 118.72767857143484} + {399.33333333333417, 216.62202930450439} + {420.37955338188368, 301.45369961823752} + + Style + + stroke + + Bezier + + Color + + b + 0.75663 + g + 0.756618 + r + 0.75664 + + HeadArrow + 0 + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + + + Class + LineGraphic + ControlPoints + + {-3.9999491373696401, 78.910715080442856} + {92.666683130060392, 0.22547126950667007} + + Head + + ID + 169490 + Info + 3 + + ID + 169472 + Layer + 1 + Points + + {473.33328247070392, 515.08928491955714} + {344.50002543131501, 611.77452873049333} + + Style + + stroke + + Bezier + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169444 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {31.999987284342428, -14.081351280212308} + {-32.166667938232536, 10.244050343831077} + + ID + 169471 + Layer + 1 + Points + + {344.96346869509995, 346.26317428240412} + {389.8333346048999, 328.08928298950207} + + Style + + stroke + + Bezier + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169456 + Info + 3 + + + + Class + LineGraphic + ControlPoints + + {31.999987284342428, -14.081351280212308} + {-28.500001271565793, 8.3333333333333144} + + ID + 169470 + Layer + 1 + Points + + {344.98861594084059, 323.71068461220347} + {391.1666679382335, 313.6666666666664} + + Style + + stroke + + Bezier + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169455 + + + + Class + LineGraphic + ControlPoints + + {31.999987284342428, -14.081351280212308} + {-40, 1.5446373167492311} + + ID + 169469 + Layer + 1 + Points + + {344.9995533451783, 301.1612218744512} + {394.50000127156665, 299.00000000000045} + + Style + + stroke + + Bezier + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169442 + Info + 3 + + + + Class + LineGraphic + ControlPoints + + {8.5833282470703125, -10.244596987647753} + {0, 0} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169457 + Info + 4 + + ID + 169468 + Layer + 1 + Points + + {272.41667175292969, 509.40064951817902} + {285.5, 503.81699882234847} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 169496 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169448 + Info + 4 + + ID + 169467 + Layer + 1 + Points + + {82.999997456869679, 300.27229985501288} + {238.34892458824362, 260.57913893040109} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169440 + Position + 0.51973581314086914 + + + + Class + Group + Graphics + + + Bounds + {{419.66662216186666, 214.61452811107179}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169460 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.756045 + g + 0.75004 + r + 0.994455 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 exception} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 130.51770718892479}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169461 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 internal process} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 91.940789540609359}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169462 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 external process (middleware, tween)} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 158.54998334248924}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169463 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 186.58225949605369}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169464 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 callback} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 63.908513387045019}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169465 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 event} + VerticalPad + 0 + + + + Bounds + {{406.9999504089372, 42.910746256511771}, {132.66667175292969, 207.81692504882812}} + Class + ShapedGraphic + ID + 169466 + Magnets + + {1, 0.5} + {1, -0.5} + {-1, 0.5} + {-1, -0.5} + {0.5, 1} + {-0.5, 1} + {0.5, -1} + {-0.5, -1} + + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + CornerRadius + 5 + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\b\fs20 \cf0 Legend} + VerticalPad + 0 + + TextPlacement + 0 + + + ID + 169459 + Layer + 1 + + + Bounds + {{233.5000012715667, 20.000000000000934}, {116, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + FontInfo + + Font + Helvetica + Size + 12 + + ID + 169458 + Layer + 1 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\b\fs24 \cf0 <%Canvas%>} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{285.5, 492.544677734375}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169457 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 BeforeRender} + VerticalPad + 0 + + + + Bounds + {{238.83337529500417, 335.17855853126167}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169456 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 HTTPForbidden} + VerticalPad + 0 + + + + Bounds + {{238.83337529500417, 312.54463200342093}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169455 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 PredicateMismatch} + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {0, 7.05596923828125} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169486 + Info + 2 + + ID + 169454 + Layer + 1 + Points + + {83.000001850648417, 128.2276785555359} + {82.999949137370791, 166.28868579864502} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169446 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169446 + + ID + 169453 + Layer + 1 + Points + + {83.000002543132396, 67.727678571434836} + {83.000002543132396, 105.18303707668386} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169445 + Info + 1 + + + + Bounds + {{30.166670481364818, 671.51189931233432}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169452 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 middleware egress} + VerticalPad + 0 + + + + Bounds + {{239, 629.51189931233432}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169451 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 finished callbacks} + VerticalPad + 0 + + + + Bounds + {{239, 572.5118993123325}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169450 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 response callbacks} + VerticalPad + 0 + + + + Bounds + {{30.166670481364818, 548.10604731241608}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169449 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 tween egress} + VerticalPad + 0 + + + + Bounds + {{238.83336130777977, 249.18303989230026}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169448 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 ContextFound} + VerticalPad + 0 + + + + Bounds + {{30.166670481364818, 240.18303707668389}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169447 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 traversal} + VerticalPad + 0 + + + + Bounds + {{30.166670481364818, 105.18303707668386}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169446 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 tween ingress} + VerticalPad + 0 + + + + Bounds + {{30.166670481364818, 45.18303707668386}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169445 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 middleware ingress } + VerticalPad + 0 + + + + Bounds + {{420.49995040893634, 472.78869058972316}, {105.66666412353516, 42.300594329833984}} + Class + ShapedGraphic + ID + 169444 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {-0.49999999999999956, -0.5} + {-0.49999999999999956, 0.5} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 notfound_view / forbidden_view / exception_view} + VerticalPad + 0 + + + + Bounds + {{420.49995040893634, 290.66666666666691}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169443 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.756045 + g + 0.75004 + r + 0.994455 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 exception} + VerticalPad + 0 + + + + Bounds + {{238.83336512247806, 289.91071033477789}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169442 + Layer + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 HTTPNotFound} + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169447 + + ID + 169441 + Layer + 1 + Points + + {82.999949137370791, 216.62202930450437} + {83.000002543132396, 240.18303707668389} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169487 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {0, 7.05596923828125} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169480 + Info + 2 + + ID + 169440 + Layer + 1 + Points + + {82.999997456869679, 263.22767855635425} + {82.999997456869679, 335} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169447 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -8.9999999999999432} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169475 + + ID + 169439 + Layer + 1 + Points + + {82.999997456869679, 385.33334350585938} + {83.000002543132396, 452.41914073626253} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169481 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9839935302734375} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169452 + Info + 2 + + ID + 169438 + Layer + 1 + Points + + {83.000002543132396, 571.15068879140836} + {83.000002543132396, 671.51189931233421} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169449 + + + + Class + LineGraphic + ControlPoints + + {0, 7.055999755859375} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169483 + Info + 2 + + ID + 169437 + Layer + 1 + Points + + {473.33328247070392, 313.2113088426139} + {473.33338419596407, 338.15028762817326} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169443 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840011596679688} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169444 + + ID + 169436 + Layer + 1 + Points + + {473.33338359264764, 388.98363112285512} + {473.33328247070392, 472.78869058972316} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169484 + + + + Class + LineGraphic + Head + + ID + 169358 + Info + 6 + + ID + 169359 + Layer + 2 + Points + + {272.4166717529298, 537.32234122436705} + {420.4999504089364, 515.08928491955714} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169206 + Info + 5 + + + + Class + LineGraphic + Head + + ID + 169358 + + ID + 169360 + Layer + 2 + Points + + {272.50004831949906, 391.51558277923863} + {420.4999504089364, 472.78869058972316} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169205 + Info + 5 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169044 + + ID + 169130 + Layer + 2 + Points + + {83.000002543132396, 592.81693102013151} + {239, 583.78422005970799} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169128 + Position + 0.28820157051086426 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169045 + + ID + 169129 + Layer + 2 + Points + + {83.000002543132396, 629.80996162681686} + {239, 640.78422005970981} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169128 + Position + 0.5668826699256897 + + + + Class + Group + Graphics + + + Bounds + {{166.8333613077798, 391.51558277923863}, {105.66668701171875, 18.656048080136394}} + Class + ShapedGraphic + ID + 169205 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, -0.49999999999999645} + {-0.49526813868737474, -0.4689979626999552} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 authorization} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 518.66629314423074}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169206 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, 0.5} + {-0.49999999999999911, 0.49999999999999289} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 decorators egress} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 410.17162450154819}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169207 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, -0.49999999999999645} + {-0.49526813868737474, -0.4689979626999552} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 decorators ingress} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 500.07262547811081}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169208 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 response adapter} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 481.41657294757954}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169209 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view mapper egress} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 447.88119486967923}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169210 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view} + VerticalPad + 0 + + + + Bounds + {{166.75000762939453, 428.77906519094307}, {105.66666412353516, 18.656048080136394}} + Class + ShapedGraphic + ID + 169211 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view mapper ingress} + VerticalPad + 0 + + + + ID + 169204 + Layer + 2 + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169085 + Info + 4 + + ID + 169086 + Layer + 2 + Points + + {83.166643778483959, 611.77452873049333} + {238.8333613077798, 611.77452873049333} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + + + Bounds + {{238.83336130777977, 600.50220798311784}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169085 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 NewResponse} + VerticalPad + 0 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169083 + Info + 4 + + ID + 169084 + Layer + 2 + Points + + {82.999986314263907, 140.3328574622312} + {239.83340199788393, 141.59152244387357} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169048 + Position + 0.35945424437522888 + + + + Bounds + {{239.83340199788395, 130.31920169649808}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169083 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 NewRequest} + VerticalPad + 0 + + + + Class + TableGroup + Graphics + + + Bounds + {{30.166605631511416, 166.28868579864499}, {105.66668701171875, 33.08929443359375}} + Class + ShapedGraphic + ID + 169081 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 URL dispatch} + VerticalPad + 0 + + + + Bounds + {{30.166605631511416, 199.37798023223874}, {105.66668701171875, 17.244049072265625}} + Class + ShapedGraphic + ID + 169082 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 route predicates} + VerticalPad + 0 + + + + GridH + + 169081 + 169082 + + + ID + 169080 + Layer + 2 + + + Class + TableGroup + Graphics + + + Bounds + {{420.5000406901047, 338.15028762817326}, {105.66668701171875, 33.08929443359375}} + Class + ShapedGraphic + ID + 169355 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view lookup} + VerticalPad + 0 + + + + Bounds + {{420.5000406901047, 371.23958206176701}, {105.66668701171875, 17.244049072265625}} + Class + ShapedGraphic + ID + 169356 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 predicates} + VerticalPad + 0 + + + + GridH + + 169355 + 169356 + + + ID + 169354 + Layer + 2 + + + Class + TableGroup + Graphics + + + Bounds + {{30.166661580404835, 335}, {105.66667175292969, 33.08929443359375}} + Class + ShapedGraphic + ID + 169075 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view lookup} + VerticalPad + 0 + + + + Bounds + {{30.166661580404835, 368.08929443359375}, {105.66667175292969, 17.244049072265625}} + Class + ShapedGraphic + ID + 169076 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 predicates} + VerticalPad + 0 + + + + GridH + + 169075 + 169076 + + + ID + 169074 + Layer + 2 + + + Class + LineGraphic + Head + + ID + 169070 + Info + 7 + + ID + 169073 + Layer + 2 + Points + + {166.75000762939462, 537.32234122436694} + {135.66666666666765, 485} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169206 + Info + 6 + + + + Class + LineGraphic + Head + + ID + 169070 + Info + 8 + + ID + 169072 + Layer + 2 + Points + + {167.33336141608385, 392.09395827769049} + {135.66666666666777, 452.41914073626253} + + Style + + stroke + + Color + + b + 0.755269 + g + 0.755239 + r + 0.75529 + + HeadArrow + 0 + Legacy + + Pattern + 11 + TailArrow + 0 + + + Tail + + ID + 169205 + Info + 6 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -8.9999999999999432} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169043 + + ID + 169071 + Layer + 2 + Points + + {83.000002543132396, 485.50842372576449} + {83.000002543132396, 548.10604731241608} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169070 + + + + Bounds + {{30.166670481364818, 452.41914073626253}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169070 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {-0.49211360058019871, -0.49251945318722434} + {-0.49211360058019871, 0.49470854679786669} + {0.4984227008620481, 0.48463479169597612} + {0.49842270086204898, -0.5} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view pipeline} + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {51.333333333333314, 0} + {-0.66666666666662877, 58.666666666666686} + {0.66673293066804717, -58.666850540458825} + {-16.306719354194399, 0.26652623861849634} + + Head + + ID + 169344 + Info + 4 + + ID + 169345 + Layer + 2 + Points + + {369.66666666666669, 541} + {404.00000000000023, 362} + {420.36749776329049, 302.42112495959606} + + Style + + stroke + + Bezier + + Color + + b + 0.75663 + g + 0.756618 + r + 0.75664 + + HeadArrow + 0 + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + + + Class + LineGraphic + ControlPoints + + {69.833333333332462, -0.72767857143483639} + {-0.66690523835279691, -51.044605218028948} + {0.66666666666674246, 51.044637362162291} + {-24.333271383961971, -0.13425428344572765} + + Head + + ID + 169344 + Info + 4 + + ID + 169346 + Layer + 2 + Points + + {310.66666666666754, 118.72767857143484} + {399.33333333333417, 216.62202930450439} + {420.37955338188368, 301.45369961823752} + + Style + + stroke + + Bezier + + Color + + b + 0.75663 + g + 0.756618 + r + 0.75664 + + HeadArrow + 0 + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + + + Class + LineGraphic + ControlPoints + + {-3.9999491373696401, 78.910715080442856} + {92.666683130060392, 0.22547126950667007} + + Head + + ID + 169085 + Info + 3 + + ID + 169361 + Layer + 2 + Points + + {473.33328247070392, 515.08928491955714} + {344.50002543131501, 611.77452873049333} + + Style + + stroke + + Bezier + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169358 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {31.999987284342428, -14.081351280212308} + {-32.166667938232536, 10.244050343831077} + + ID + 169341 + Layer + 2 + Points + + {344.96346869509995, 346.26317428240412} + {389.8333346048999, 328.08928298950207} + + Style + + stroke + + Bezier + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169340 + Info + 3 + + + + Class + LineGraphic + ControlPoints + + {31.999987284342428, -14.081351280212308} + {-28.500001271565793, 8.3333333333333144} + + ID + 169337 + Layer + 2 + Points + + {344.98861594084059, 323.71068461220347} + {391.1666679382335, 313.6666666666664} + + Style + + stroke + + Bezier + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169336 + + + + Class + LineGraphic + ControlPoints + + {31.999987284342428, -14.081351280212308} + {-40, 1.5446373167492311} + + ID + 169333 + Layer + 2 + Points + + {344.9995533451783, 301.1612218744512} + {394.50000127156665, 299.00000000000045} + + Style + + stroke + + Bezier + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169332 + Info + 3 + + + + Class + LineGraphic + ControlPoints + + {8.5833282470703125, -10.244596987647753} + {0, 0} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169051 + Info + 4 + + ID + 169062 + Layer + 2 + Points + + {272.41667175292969, 509.40064951817902} + {285.5, 503.81699882234847} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 169208 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169042 + Info + 4 + + ID + 169061 + Layer + 2 + Points + + {82.999997456869679, 300.27229985501288} + {238.34892458824362, 260.57913893040109} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169034 + Position + 0.51973581314086914 + + + + Class + Group + Graphics + + + Bounds + {{419.66662216186666, 214.61452811107179}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169054 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.756045 + g + 0.75004 + r + 0.994455 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 exception} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 130.51770718892479}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169055 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 internal process} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 91.940789540609359}, {105.66666412353516, 33.089282989501953}} + Class + ShapedGraphic + ID + 169056 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 external process (middleware, tween)} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 158.54998334248924}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169057 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 186.58225949605369}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169058 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 callback} + VerticalPad + 0 + + + + Bounds + {{419.66662216186666, 63.908513387045019}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169059 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 event} + VerticalPad + 0 + + + + Bounds + {{406.9999504089372, 42.910746256511771}, {132.66667175292969, 207.81692504882812}} + Class + ShapedGraphic + ID + 169060 + Magnets + + {1, 0.5} + {1, -0.5} + {-1, 0.5} + {-1, -0.5} + {0.5, 1} + {-0.5, 1} + {0.5, -1} + {-0.5, -1} + + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + CornerRadius + 5 + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\b\fs20 \cf0 Legend} + VerticalPad + 0 + + TextPlacement + 0 + + + ID + 169053 + Layer + 2 + + + Bounds + {{233.5000012715667, 20.000000000000934}, {116, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + FontInfo + + Font + Helvetica + Size + 12 + + ID + 169052 + Layer + 2 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\b\fs24 \cf0 <%Canvas%>} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{285.5, 492.544677734375}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169051 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 BeforeRender} + VerticalPad + 0 + + + + Bounds + {{238.83337529500417, 335.17855853126167}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169340 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 HTTPForbidden} + VerticalPad + 0 + + + + Bounds + {{238.83337529500417, 312.54463200342093}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169336 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 PredicateMismatch} + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {0, 7.05596923828125} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169081 + Info + 2 + + ID + 169048 + Layer + 2 + Points + + {83.000001850648417, 128.2276785555359} + {82.999949137370791, 166.28868579864502} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169040 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169040 + + ID + 169047 + Layer + 2 + Points + + {83.000002543132396, 67.727678571434836} + {83.000002543132396, 105.18303707668386} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169039 + Info + 1 + + + + Bounds + {{30.166670481364818, 671.51189931233432}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169046 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 middleware egress} + VerticalPad + 0 + + + + Bounds + {{239, 629.51189931233432}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169045 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 finished callbacks} + VerticalPad + 0 + + + + Bounds + {{239, 572.5118993123325}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169044 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.327428 + g + 0.81823 + r + 0.995566 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 response callbacks} + VerticalPad + 0 + + + + Bounds + {{30.166670481364818, 548.10604731241608}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169043 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 tween egress} + VerticalPad + 0 + + + + Bounds + {{238.83336130777977, 249.18303989230026}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169042 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 ContextFound} + VerticalPad + 0 + + + + Bounds + {{30.166670481364818, 240.18303707668389}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169041 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 traversal} + VerticalPad + 0 + + + + Bounds + {{30.166670481364818, 105.18303707668386}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169040 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 tween ingress} + VerticalPad + 0 + + + + Bounds + {{30.166670481364818, 45.18303707668386}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169039 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999208 + g + 0.811343 + r + 0.644457 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 middleware ingress } + VerticalPad + 0 + + + + Bounds + {{420.49995040893634, 472.78869058972316}, {105.66666412353516, 42.300594329833984}} + Class + ShapedGraphic + ID + 169358 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {-0.49999999999999956, -0.5} + {-0.49999999999999956, 0.5} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 notfound_view / forbidden_view / exception_view} + VerticalPad + 0 + + + + Bounds + {{420.49995040893634, 290.66666666666691}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169344 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.756045 + g + 0.75004 + r + 0.994455 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 exception} + VerticalPad + 0 + + + + Bounds + {{238.83336512247806, 289.91071033477789}, {105.66666412353516, 22.544642175946908}} + Class + ShapedGraphic + ID + 169332 + Layer + 2 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 HTTPNotFound} + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169041 + + ID + 169035 + Layer + 2 + Points + + {82.999949137370791, 216.62202930450437} + {83.000002543132396, 240.18303707668389} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169082 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {0, 7.05596923828125} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169075 + Info + 2 + + ID + 169034 + Layer + 2 + Points + + {82.999997456869679, 263.22767855635425} + {82.999997456869679, 335} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169041 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -8.9999999999999432} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169070 + + ID + 169033 + Layer + 2 + Points + + {82.999997456869679, 385.33334350585938} + {83.000002543132396, 452.41914073626253} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169076 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9839935302734375} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169046 + Info + 2 + + ID + 169128 + Layer + 2 + Points + + {83.000002543132396, 571.15068879140836} + {83.000002543132396, 671.51189931233421} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169043 + + + + Class + LineGraphic + ControlPoints + + {0, 7.055999755859375} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169355 + Info + 2 + + ID + 169357 + Layer + 2 + Points + + {473.33328247070392, 313.2113088426139} + {473.33338419596407, 338.15028762817326} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169344 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840011596679688} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169358 + + ID + 169362 + Layer + 2 + Points + + {473.33338359264764, 388.98363112285512} + {473.33328247070392, 472.78869058972316} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169356 + + + + GridInfo + + GuidesLocked + NO + GuidesVisible + YES + HPages + 1 + ImageCounter + 3 + KeepToScale + + Layers + + + Lock + NO + Name + no exceptions + Print + YES + View + YES + + + Lock + NO + Name + exceptions only + Print + YES + View + NO + + + Lock + NO + Name + all + Print + YES + View + NO + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + LinksVisible + NO + MagnetsVisible + NO + MasterSheets + + ModificationDate + 2014-11-23 07:19:11 +0000 + Modifier + Steve Piercy + NotesVisible + NO + Orientation + 2 + OriginVisible + NO + PageBreaks + YES + PrintInfo + + NSBottomMargin + + float + 41 + + NSHorizonalPagination + + coded + BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG + + NSLeftMargin + + float + 18 + + NSPaperSize + + size + {612, 792} + + NSPrintReverseOrientation + + int + 0 + + NSRightMargin + + float + 18 + + NSTopMargin + + float + 18 + + + PrintOnePage + + ReadOnly + NO + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Request Processing + SmartAlignmentGuidesActive + YES + SmartDistanceGuidesActive + YES + UniqueID + 1 + UseEntirePage + + VPages + 1 + WindowInfo + + CurrentSheet + 0 + ExpandedCanvases + + + name + Request Processing + + + Frame + {{35, 93}, {1394, 1325}} + ListView + + OutlineWidth + 178 + RightSidebar + + ShowRuler + + Sidebar + + SidebarWidth + 163 + VisibleRegion + {{-231, -226}, {1037, 1186}} + Zoom + 1 + ZoomValues + + + Request Processing + 1 + 2 + + + + + diff --git a/docs/_static/pyramid_request_processing.png b/docs/_static/pyramid_request_processing.png new file mode 100644 index 000000000..2fbb1e164 Binary files /dev/null and b/docs/_static/pyramid_request_processing.png differ diff --git a/docs/_static/pyramid_request_processing.svg b/docs/_static/pyramid_request_processing.svg new file mode 100644 index 000000000..21bbcb532 --- /dev/null +++ b/docs/_static/pyramid_request_processing.svg @@ -0,0 +1,3 @@ + + +2014-11-23 07:19ZRequest Processingno exceptionsmiddleware ingress tween ingresstraversalContextFoundtween egressresponse callbacksfinished callbacksmiddleware egressBeforeRenderRequest ProcessingLegendeventcallbackviewexternal process (middleware, tween)internal processview pipelinepredicatesview lookuproute predicatesURL dispatchNewRequestNewResponseview mapper ingressviewview mapper egressresponse adapterdecorators ingressdecorators egressauthorization diff --git a/docs/_static/pyramid_router.svg b/docs/_static/pyramid_router.svg deleted file mode 100644 index 21bbcb532..000000000 --- a/docs/_static/pyramid_router.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -2014-11-23 07:19ZRequest Processingno exceptionsmiddleware ingress tween ingresstraversalContextFoundtween egressresponse callbacksfinished callbacksmiddleware egressBeforeRenderRequest ProcessingLegendeventcallbackviewexternal process (middleware, tween)internal processview pipelinepredicatesview lookuproute predicatesURL dispatchNewRequestNewResponseview mapper ingressviewview mapper egressresponse adapterdecorators ingressdecorators egressauthorization diff --git a/docs/narr/router.rst b/docs/narr/router.rst index 745c2faa1..e82b66801 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -9,7 +9,7 @@ Request Processing ================== -.. image:: ../_static/pyramid_router.svg +.. image:: ../_static/pyramid_request_processing.svg :alt: Request Processing Once a :app:`Pyramid` application is up and running, it is ready to accept -- cgit v1.2.3 From 29e7a7d80149e84b844b68f093a7676ba9e400ee Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 1 Dec 2014 13:45:01 -0800 Subject: replace router.png with pyramid_router.svg and make design consistent --- docs/_static/pyramid_router.graffle | 1621 +++++++++++++++++++++++++++++++++++ docs/_static/pyramid_router.png | Bin 0 -> 120643 bytes docs/_static/pyramid_router.svg | 3 + docs/narr/router.rst | 3 +- 4 files changed, 1626 insertions(+), 1 deletion(-) create mode 100644 docs/_static/pyramid_router.graffle create mode 100644 docs/_static/pyramid_router.png create mode 100644 docs/_static/pyramid_router.svg diff --git a/docs/_static/pyramid_router.graffle b/docs/_static/pyramid_router.graffle new file mode 100644 index 000000000..217878426 --- /dev/null +++ b/docs/_static/pyramid_router.graffle @@ -0,0 +1,1621 @@ + + + + + ActiveLayerIndex + 0 + ApplicationVersion + + com.omnigroup.OmniGrafflePro + 139.18.0.187838 + + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {576, 733}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + BaseZoom + 0 + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + CreationDate + 2014-12-01 08:25:13 +0000 + Creator + Steve Piercy + DisplayScale + 1 0/72 in = 1 0/72 in + GraphDocumentVersion + 8 + GraphicsList + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169413 + + ID + 169414 + Points + + {202.04165903727232, 501.05557886759294} + {202.04165903727232, 528.77776209513161} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169412 + + + + Bounds + {{104.41666666666686, 528.77776209513161}, {195.24998474121094, 29}} + Class + ShapedGraphic + ID + 169413 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Return the +\b response} + VerticalPad + 0 + + + + Bounds + {{104.41666666666657, 471.55557886759294}, {195.24998474121094, 29}} + Class + ShapedGraphic + ID + 169412 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Invoke the +\b view callable +\b0 ,\ +which returns a +\b response} + VerticalPad + 0 + + + + Bounds + {{291.21562524160186, 379.55555343627816}, {26, 24}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + ID + 169411 + Line + + ID + 169410 + Offset + 7.3333320617675781 + Position + 0.4865129292011261 + RotationType + 0 + + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 No} + + Wrap + NO + + + Class + LineGraphic + ControlPoints + + {34.791667904111534, 0} + {-33.999994913736998, 0} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169409 + + ID + 169410 + Points + + {280.85416589389337, 398.88888549804574} + {327.47912214508739, 398.88888549804574} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169404 + + + + Bounds + {{327.47912214508739, 384.38888549804574}, {156.62496948242188, 29}} + Class + ShapedGraphic + ID + 169409 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.756045 + g + 0.75004 + r + 0.994455 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Return the +\b Forbidden View} + VerticalPad + 0 + + + + Bounds + {{175.11595161998204, 438.9999954213917}, {30, 24}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + ID + 169408 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Yes} + + Wrap + NO + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169412 + Info + 2 + + ID + 169407 + Points + + {202.04165267944353, 437.33333079020139} + {202.04165903727204, 471.55557886759294} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169404 + Info + 1 + + + + Bounds + {{171.708317756653, 329.24978243601743}, {30, 24}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + ID + 169406 + Line + + ID + 169405 + Offset + -15.333334922790527 + Position + 0.45895844697952271 + RotationType + 0 + + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Yes} + + Wrap + NO + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169404 + Info + 2 + + ID + 169405 + Points + + {202.04165267944353, 326.72223360222029} + {202.04165267944353, 360.44446818033811} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 3 + + + + Bounds + {{123.72916793823259, 360.44446818033811}, {156.62496948242188, 76.888862609863281}} + Class + ShapedGraphic + ID + 169404 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Diamond + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Current user has +\b authorization +\b0 to invoke the view callable?} + VerticalPad + 0 + + + + Bounds + {{283.07625736262997, 281.88889694213805}, {26, 24}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + ID + 169403 + Line + + ID + 169402 + Offset + 7.3333320617675781 + Position + 0.4865129292011261 + RotationType + 0 + + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 No} + + Wrap + NO + + + Class + LineGraphic + ControlPoints + + {34.791667904111534, 0} + {-33.999994913736998, 0} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169401 + + ID + 169402 + Points + + {265.20833208871704, 301.22222900390562} + {327.47911580403627, 301.22222900390562} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 3 + + + + Bounds + {{327.47911580403627, 286.72222900390562}, {156.62496948242188, 29}} + Class + ShapedGraphic + ID + 169401 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.756045 + g + 0.75004 + r + 0.994455 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Return the +\b Not Found View} + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 3 + Info + 2 + + ID + 169400 + Points + + {202.04165903727255, 251} + {202.04165776570633, 276.22223154703772} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169393 + + + + Bounds + {{139.37498982747391, 276.22223154703778}, {125.33333587646484, 50}} + Class + ShapedGraphic + ID + 3 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Diamond + Style + + fill + + Color + + b + 0.422927 + g + 1 + r + 1 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 View callable found?} + VerticalPad + 0 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169393 + Info + 2 + + ID + 169396 + Points + + {202.04165903727255, 196.77777862548834} + {202.04165903727255, 222} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169392 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169392 + + ID + 169395 + Points + + {202.04165903727255, 142.55555725097662} + {202.04165903727255, 167.77777862548834} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 169391 + Info + 1 + + + + Class + LineGraphic + ControlPoints + + {0, 6.9840087890625} + {0, -9} + + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169391 + + ID + 169385 + Points + + {202.04165903727255, 82.666667938232479} + {202.04165903727255, 107.88888931274418} + + Style + + stroke + + Bezier + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + SharpArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 19 + Info + 1 + + + + Bounds + {{104.41666666666708, 222}, {195.24998474121094, 29}} + Class + ShapedGraphic + ID + 169393 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Look up a +\b view callable +\b0 in the +\b registry +\b0 using the +\b context +\b0 and +\b view name} + VerticalPad + 0 + + + + Bounds + {{104.41666666666708, 167.77777862548834}, {195.24998474121094, 29}} + Class + ShapedGraphic + ID + 169392 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs20 \cf0 Traversal +\b0 locates\ +the +\b context +\b0 and +\b view name} + VerticalPad + 0 + + + + Bounds + {{104.41666666666708, 107.88888931274418}, {195.24998474121094, 34.666667938232422}} + Class + ShapedGraphic + ID + 169391 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Traverse the model graph\ +from the +\b root +\b0 using the +\b path} + VerticalPad + 0 + + + + Bounds + {{104.41666666666708, 48.000000000000043}, {195.24998474121094, 34.666667938232422}} + Class + ShapedGraphic + ID + 19 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.815377 + g + 1 + r + 0.820561 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Obtain a root object from the +\b root factory} + VerticalPad + 0 + + + + Bounds + {{229.04165903727255, 20.000000000000934}, {90, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + FontInfo + + Font + Helvetica + Size + 12 + + ID + 169390 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\b\fs24 \cf0 <%Canvas%>} + VerticalPad + 0 + + Wrap + NO + + + GridInfo + + GuidesLocked + NO + GuidesVisible + YES + HPages + 1 + ImageCounter + 1 + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + LinksVisible + NO + MagnetsVisible + NO + MasterSheets + + ModificationDate + 2014-12-01 09:19:51 +0000 + Modifier + Steve Piercy + NotesVisible + NO + Orientation + 2 + OriginVisible + NO + PageBreaks + YES + PrintInfo + + NSBottomMargin + + float + 41 + + NSHorizonalPagination + + coded + BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG + + NSLeftMargin + + float + 18 + + NSPaperSize + + size + {612, 792} + + NSPrintReverseOrientation + + int + 0 + + NSRightMargin + + float + 18 + + NSTopMargin + + float + 18 + + + PrintOnePage + + ReadOnly + NO + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Pyramid Router + SmartAlignmentGuidesActive + YES + SmartDistanceGuidesActive + YES + UniqueID + 1 + UseEntirePage + + VPages + 1 + WindowInfo + + CurrentSheet + 0 + ExpandedCanvases + + + name + Pyramid Router + + + Frame + {{96, 20}, {1076, 1286}} + ListView + + OutlineWidth + 142 + RightSidebar + + ShowRuler + + Sidebar + + SidebarWidth + 120 + VisibleRegion + {{8, -10}, {532, 754.66666666666663}} + Zoom + 1.5 + ZoomValues + + + Pyramid Router + 1.5 + 1 + + + + + diff --git a/docs/_static/pyramid_router.png b/docs/_static/pyramid_router.png new file mode 100644 index 000000000..3c9f81158 Binary files /dev/null and b/docs/_static/pyramid_router.png differ diff --git a/docs/_static/pyramid_router.svg b/docs/_static/pyramid_router.svg new file mode 100644 index 000000000..1537777c9 --- /dev/null +++ b/docs/_static/pyramid_router.svg @@ -0,0 +1,3 @@ + + +2014-12-01 09:19ZPyramid RouterLayer 1Pyramid RouterObtain a root object from the root factoryTraverse the model graphfrom the root using the pathTraversal locatesthe context and view nameLook up a view callable in the registry using the context and view nameView callable found?Return the Not Found ViewNoCurrent user has authorization to invoke the view callable?YesYesReturn the Forbidden ViewNoInvoke the view callable,which returns a responseReturn the response diff --git a/docs/narr/router.rst b/docs/narr/router.rst index e82b66801..693217a6b 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -119,7 +119,8 @@ request enters a :app:`Pyramid` application through to the point that #. The :term:`thread local` stack is popped. -.. image:: router.png +.. image:: ../_static/pyramid_router.svg + :alt: Pyramid Router This is a very high-level overview that leaves out various details. For more detail about subsystems invoked by the :app:`Pyramid` router such as -- cgit v1.2.3 From f0a9dfba27012bcf84fccd445b0fcaa1bca32382 Mon Sep 17 00:00:00 2001 From: Laurence Rowe Date: Fri, 5 Dec 2014 17:37:57 -0800 Subject: Support returning app_iter from renderer. Sets `response.app_iter = result` for renderer results which are iterable and not text/bytes. --- pyramid/renderers.py | 4 ++++ pyramid/tests/test_renderers.py | 25 +++++++++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 108255ee4..2addaa5fe 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -456,6 +456,10 @@ class RendererHelper(object): if result is not None: if isinstance(result, text_type): response.text = result + elif isinstance(result, bytes): + response.body = result + elif hasattr(result, '__iter__'): + response.app_iter = result else: response.body = result diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 2bddd2318..bb5b0455a 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -191,8 +191,8 @@ class TestRendererHelper(unittest.TestCase): helper = self._makeOne('loo.foo') response = helper.render_to_response('values', {}, request=request) - self.assertEqual(response.body[0], 'values') - self.assertEqual(response.body[1], {}) + self.assertEqual(response.app_iter[0], 'values') + self.assertEqual(response.app_iter[1], {}) def test_get_renderer(self): factory = self._registerRendererFactory() @@ -209,8 +209,8 @@ class TestRendererHelper(unittest.TestCase): request = testing.DummyRequest() response = 'response' response = helper.render_view(request, response, view, context) - self.assertEqual(response.body[0], 'response') - self.assertEqual(response.body[1], + self.assertEqual(response.app_iter[0], 'response') + self.assertEqual(response.app_iter[1], {'renderer_info': helper, 'renderer_name': 'loo.foo', 'request': request, @@ -287,6 +287,23 @@ class TestRendererHelper(unittest.TestCase): response = helper._make_response(la.encode('utf-8'), request) self.assertEqual(response.body, la.encode('utf-8')) + def test__make_response_result_is_iterable(self): + from pyramid.response import Response + request = testing.DummyRequest() + request.response = Response() + helper = self._makeOne('loo.foo') + la = text_('/La Pe\xc3\xb1a', 'utf-8') + response = helper._make_response([la.encode('utf-8')], request) + self.assertEqual(response.body, la.encode('utf-8')) + + def test__make_response_result_is_other(self): + self._registerResponseFactory() + request = None + helper = self._makeOne('loo.foo') + result = object() + response = helper._make_response(result, request) + self.assertEqual(response.body, result) + def test__make_response_result_is_None_no_body(self): from pyramid.response import Response request = testing.DummyRequest() -- cgit v1.2.3 From cc15bbf7de74f4cdfc676e34fa429d2658d1ddf6 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 9 Dec 2014 11:00:41 -0500 Subject: Move coverage floor pct into 'setup.cfg'. --- setup.cfg | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index a877ffb7f..9633b6980 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,6 +7,7 @@ where=pyramid nocapture=1 cover-package=pyramid cover-erase=1 +cover-min-percentage=100 [aliases] dev = develop easy_install pyramid[testing] diff --git a/tox.ini b/tox.ini index 3f32dbc3f..714c5b6d3 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 --cover-min-percentage=100 + python setup.py nosetests --with-xunit --with-xcoverage deps = nosexcover -- cgit v1.2.3 From bb60b86feeea7cfbb531460b22ad40f211562708 Mon Sep 17 00:00:00 2001 From: Zack Marvel Date: Wed, 10 Dec 2014 01:25:11 -0500 Subject: Revise URL Dispatch documentation to use config.scan() in Examples 1, 2, and 3 In response to #600. --- docs/narr/urldispatch.rst | 57 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 87a962a9a..2fd971917 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -495,17 +495,20 @@ result in a particular view callable being invoked: :linenos: config.add_route('idea', 'site/{id}') - config.add_view('mypackage.views.site_view', route_name='idea') + config.scan() When a route configuration with a ``view`` attribute is added to the system, and an incoming request matches the *pattern* of the route configuration, the :term:`view callable` named as the ``view`` attribute of the route configuration will be invoked. -In the case of the above example, when the URL of a request matches -``/site/{id}``, the view callable at the Python dotted path name -``mypackage.views.site_view`` will be called with the request. In other -words, we've associated a view callable directly with a route pattern. +Recall that ``config.scan`` is equivalent to calling ``config.add_view``, +because the ``@view_config`` decorator in ``mypackage.views``, shown below, +maps the route name to the matching view callable. In the case of the above +example, when the URL of a request matches ``/site/{id}``, the view callable at +the Python dotted path name ``mypackage.views.site_view`` will be called with +the request. In other words, we've associated a view callable directly with a +route pattern. When the ``/site/{id}`` route pattern matches during a request, the ``site_view`` view callable is invoked with that request as its sole @@ -519,8 +522,10 @@ The ``mypackage.views`` module referred to above might look like so: .. code-block:: python :linenos: + from pyramid.view import view_config from pyramid.response import Response + @view_config(route_name='idea') def site_view(request): return Response(request.matchdict['id']) @@ -542,11 +547,30 @@ add to your application: config.add_route('idea', 'ideas/{idea}') config.add_route('user', 'users/{user}') config.add_route('tag', 'tags/{tag}') + config.scan() + +Here is an example of a corresponding ``mypackage.views`` module: - config.add_view('mypackage.views.idea_view', route_name='idea') - config.add_view('mypackage.views.user_view', route_name='user') - config.add_view('mypackage.views.tag_view', route_name='tag') +.. code-block:: python + :linenos: + + from pyramid.view import view_config + from pyramid.response import Response + @view_config(route_name='idea') + def idea_view(request): + return Response(request.matchdict['id']) + + @view_config(route_name='user') + def user_view(request): + user = request.matchdict['user'] + return Response(u'The user is {}.'.format(user)) + + @view_config(route_name='tag') + def tag_view(request): + tag = request.matchdict['tag'] + return Response(u'The tag is {}.'.format(tag)) + The above configuration will allow :app:`Pyramid` to service URLs in these forms: @@ -596,7 +620,7 @@ An example of using a route with a factory: :linenos: config.add_route('idea', 'ideas/{idea}', factory='myproject.resources.Idea') - config.add_view('myproject.views.idea_view', route_name='idea') + config.scan() The above route will manufacture an ``Idea`` resource as a :term:`context`, assuming that ``mypackage.resources.Idea`` resolves to a class that accepts a @@ -610,7 +634,20 @@ request in its ``__init__``. For example: pass In a more complicated application, this root factory might be a class -representing a :term:`SQLAlchemy` model. +representing a :term:`SQLAlchemy` model. The view ``mypackage.views.idea_view`` +might look like this: + +.. code-block:: python + :linenos: + + @view_config(route_name='idea') + def idea_view(request): + idea = request.context + return Response(idea) + +Here, ``request.context`` is an instance of ``Idea``. If indeed the resource +object is a SQLAlchemy model, you do not even have to perform a query in the +view callable, since you have access to the resource via ``request.context``. See :ref:`route_factories` for more details about how to use route factories. -- cgit v1.2.3 From 1dfd12a21edba88f19d3f9af3ba6d127a461512d Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 14 Dec 2014 20:14:08 -0800 Subject: Fix checks for @wsgiapp2, MultiView, and add request method --- pyramid/scripts/proutes.py | 288 +++++++++++++++++++++-------- pyramid/tests/test_scripts/test_proutes.py | 255 +++++++++++++++++++++++-- 2 files changed, 452 insertions(+), 91 deletions(-) diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index d0c1aa13e..b9a33f5bc 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -3,10 +3,22 @@ import sys import textwrap from pyramid.paster import bootstrap +from pyramid.compat import string_types +from pyramid.interfaces import ( + IRouteRequest, + IViewClassifier, + IView, +) + from pyramid.scripts.common import parse_vars +from pyramid.static import static_view +from zope.interface import Interface +from collections import OrderedDict PAD = 3 +ANY_KEY = '*' +UNKNOWN_KEY = '' def main(argv=sys.argv, quiet=False): @@ -14,6 +26,165 @@ def main(argv=sys.argv, quiet=False): return command.run() +def _get_pattern(route): + pattern = route.pattern + + if not pattern.startswith('/'): + pattern = '/%s' % pattern + return pattern + + +def _get_print_format(max_name, max_pattern, max_view, max_method): + fmt = '%-{0}s %-{1}s %-{2}s %-{3}s'.format( + max_name + PAD, + max_pattern + PAD, + max_view + PAD, + max_method + PAD, + ) + return fmt + + +def _get_request_methods(route_request_methods, view_request_methods): + has_route_methods = route_request_methods is not None + has_view_methods = len(view_request_methods) > 0 + has_methods = has_route_methods or has_view_methods + + if has_route_methods is False and has_view_methods is False: + request_methods = [ANY_KEY] + elif has_route_methods is False and has_view_methods is True: + request_methods = view_request_methods + elif has_route_methods is True and has_view_methods is False: + request_methods = route_request_methods + else: + request_methods = set(route_request_methods).intersection( + set(view_request_methods) + ) + + if has_methods and not request_methods: + request_methods = '' + elif request_methods: + request_methods = ','.join(request_methods) + + return request_methods + + +def _get_view_module(view_callable): + if view_callable is None: + return UNKNOWN_KEY + + if hasattr(view_callable, '__name__'): + if hasattr(view_callable, '__original_view__'): + original_view = view_callable.__original_view__ + else: + original_view = None + + if isinstance(original_view, static_view): + if original_view.package_name is not None: + return '%s:%s' % ( + original_view.package_name, + original_view.docroot + ) + else: + return original_view.docroot + else: + view_name = view_callable.__name__ + else: + # Currently only MultiView hits this, + # we could just not run _get_view_module + # for them and remove this logic + view_name = str(view_callable) + + view_module = '%s.%s' % ( + view_callable.__module__, + view_name, + ) + + # If pyramid wraps something in wsgiapp or wsgiapp2 decorators + # that is currently returned as pyramid.router.decorator, lets + # hack a nice name in: + if view_module == 'pyramid.router.decorator': + view_module = '' + + return view_module + + +def get_route_data(route, registry): + pattern = _get_pattern(route) + + request_iface = registry.queryUtility( + IRouteRequest, + name=route.name + ) + + route_request_methods = None + view_request_methods = OrderedDict() + view_callable = None + + route_intr = registry.introspector.get( + 'routes', route.name + ) + + if (request_iface is None) or (route.factory is not None): + return [ + (route.name, _get_pattern(route), UNKNOWN_KEY, ANY_KEY) + ] + + view_callable = registry.adapters.lookup( + (IViewClassifier, request_iface, Interface), + IView, + name='', + default=None + ) + view_module = _get_view_module(view_callable) + # Introspectables can be turned off, so there could be a chance + # that we have no `route_intr` but we do have a route + callable + if route_intr is None: + view_request_methods[view_module] = [] + else: + route_request_methods = route_intr['request_methods'] + + view_intr = registry.introspector.related(route_intr) + + if view_intr: + for view in view_intr: + request_method = view.get('request_methods') + + if request_method is not None: + view_callable = view['callable'] + view_module = _get_view_module(view_callable) + + if view_module not in view_request_methods: + view_request_methods[view_module] = [] + + if isinstance(request_method, string_types): + request_method = (request_method,) + + view_request_methods[view_module].extend(request_method) + else: + if view_module not in view_request_methods: + view_request_methods[view_module] = [] + + else: + view_request_methods[view_module] = [] + + final_routes = [] + + for view_module, methods in view_request_methods.items(): + request_methods = _get_request_methods( + route_request_methods, + methods + ) + + final_routes.append(( + route.name, + pattern, + view_module, + request_methods, + )) + + return final_routes + + class PRoutesCommand(object): description = """\ Print all URL dispatch routes used by a Pyramid application in the @@ -34,107 +205,74 @@ class PRoutesCommand(object): parser = optparse.OptionParser( usage, description=textwrap.dedent(description) - ) + ) def __init__(self, argv, quiet=False): self.options, self.args = self.parser.parse_args(argv[1:]) self.quiet = quiet + def out(self, msg): # pragma: no cover + if not self.quiet: + print(msg) + def _get_mapper(self, registry): from pyramid.config import Configurator - config = Configurator(registry = registry) + config = Configurator(registry=registry) return config.get_routes_mapper() - 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') return 2 - 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] - env = self.bootstrap[0](config_uri, options=parse_vars(self.args[1:])) 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() - - if not routes: - return 0 - - mapped_routes.append(( - '-' * max_name, - '-' * max_pattern, - '-' * max_view, - )) - - for route in routes: - pattern = route.pattern - if not pattern.startswith('/'): - pattern = '/' + pattern - request_iface = registry.queryUtility(IRouteRequest, - name=route.name) - view_callable = None - - if (request_iface is None) or (route.factory is not None): - view_callable = '' - else: - view_callable = registry.adapters.lookup( - (IViewClassifier, request_iface, Interface), - IView, name='', default=None) - - 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 mapper is None: + return 0 + + max_name = len('Name') + max_pattern = len('Pattern') + max_view = len('View') + max_method = len('Method') + + routes = mapper.get_routes() + + if len(routes) == 0: + return 0 + + mapped_routes = [ + ('Name', 'Pattern', 'View', 'Method'), + ('----', '-------', '----', '------') + ] + + for route in routes: + route_data = get_route_data(route, registry) + + for name, pattern, view, method in route_data: + if len(name) > max_name: + max_name = len(name) if len(pattern) > max_pattern: max_pattern = len(pattern) - if len(view_callable) > max_view: - max_view = len(view_callable) + if len(view) > max_view: + max_view = len(view) - mapped_routes.append((route.name, pattern, view_callable)) + if len(method) > max_method: + max_method = len(method) - fmt = '%-{0}s %-{1}s %-{2}s'.format( - max_name + PAD, - max_pattern + PAD, - max_view + PAD, - ) + mapped_routes.append((name, pattern, view, method)) - for route_data in mapped_routes: - self.out(fmt % route_data) + fmt = _get_print_format(max_name, max_pattern, max_view, max_method) + + for route in mapped_routes: + self.out(fmt % route) return 0 -if __name__ == '__main__': # pragma: no cover + +if __name__ == '__main__': # pragma: no cover sys.exit(main() or 0) diff --git a/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py index 32202af4b..0713f4ac9 100644 --- a/pyramid/tests/test_scripts/test_proutes.py +++ b/pyramid/tests/test_scripts/test_proutes.py @@ -1,6 +1,16 @@ import unittest from pyramid.tests.test_scripts import dummy + +class DummyIntrospector(object): + def __init__(self): + self.relations = {} + self.introspectables = {} + + def get(self, name, discrim): + pass + + class TestPRoutesCommand(unittest.TestCase): def _getTargetClass(self): from pyramid.scripts.proutes import PRoutesCommand @@ -12,6 +22,17 @@ class TestPRoutesCommand(unittest.TestCase): cmd.args = ('/foo/bar/myapp.ini#myapp',) return cmd + def _makeRegistry(self): + from pyramid.registry import Registry + registry = Registry() + registry.introspector = DummyIntrospector() + return registry + + def _makeConfig(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + def test_good_args(self): cmd = self._getTargetClass()([]) cmd.bootstrap = (dummy.DummyBootstrap(),) @@ -19,6 +40,8 @@ class TestPRoutesCommand(unittest.TestCase): route = dummy.DummyRoute('a', '/a') mapper = dummy.DummyMapper(route) cmd._get_mapper = lambda *arg: mapper + registry = self._makeRegistry() + cmd.bootstrap = (dummy.DummyBootstrap(registry=registry),) L = [] cmd.out = lambda msg: L.append(msg) cmd.run() @@ -58,12 +81,15 @@ class TestPRoutesCommand(unittest.TestCase): route = dummy.DummyRoute('a', '/a') mapper = dummy.DummyMapper(route) command._get_mapper = lambda *arg: mapper + registry = self._makeRegistry() + command.bootstrap = (dummy.DummyBootstrap(registry=registry),) + L = [] command.out = L.append result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) - self.assertEqual(L[-1].split(), ['a', '/a', '']) + self.assertEqual(L[-1].split(), ['a', '/a', '', '*']) def test_route_with_no_slash_prefix(self): command = self._makeOne() @@ -72,16 +98,18 @@ class TestPRoutesCommand(unittest.TestCase): command._get_mapper = lambda *arg: mapper L = [] command.out = L.append + registry = self._makeRegistry() + command.bootstrap = (dummy.DummyBootstrap(registry=registry),) result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) - self.assertEqual(L[-1].split(), ['a', '/a', '']) + self.assertEqual(L[-1].split(), ['a', '/a', '', '*']) def test_single_route_no_views_registered(self): from zope.interface import Interface - from pyramid.registry import Registry from pyramid.interfaces import IRouteRequest - registry = Registry() + registry = self._makeRegistry() + def view():pass class IMyRoute(Interface): pass @@ -96,15 +124,15 @@ class TestPRoutesCommand(unittest.TestCase): result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) - self.assertEqual(L[-1].split()[:3], ['a', '/a', 'None']) + self.assertEqual(L[-1].split()[:3], ['a', '/a', '']) def test_single_route_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() + registry = self._makeRegistry() + def view():pass class IMyRoute(Interface): pass @@ -130,11 +158,11 @@ class TestPRoutesCommand(unittest.TestCase): 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() + registry = self._makeRegistry() + def view():pass class IMyRoute(Interface): @@ -172,11 +200,11 @@ class TestPRoutesCommand(unittest.TestCase): def test_single_route_one_view_registered_with_factory(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() + registry = self._makeRegistry() + def view():pass class IMyRoot(Interface): pass @@ -201,12 +229,11 @@ class TestPRoutesCommand(unittest.TestCase): 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() + registry = self._makeRegistry() def view(): pass @@ -235,19 +262,215 @@ class TestPRoutesCommand(unittest.TestCase): self.assertEqual(result, 0) self.assertEqual(len(L), 3) compare_to = L[-1].split()[:3] + view_module = 'pyramid.tests.test_scripts.dummy' + view_str = '' + ] + self.assertEqual(compare_to, expected) + + def test_route_static_views(self): + from pyramid.renderers import null_renderer as nr + config = self._makeConfig(autocommit=True) + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_static_view(name='static2', path='/var/www/static') + config.add_static_view( + name='pyramid_scaffold', + path='pyramid:scaffolds/starter/+package+/static' + ) + + command = self._makeOne() + L = [] + command.out = L.append + command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 5) + + expected = [ + ['__static/', '/static/*subpath', + 'pyramid.tests.test_scripts:static/', '*'], + ['__static2/', '/static2/*subpath', '/var/www/static/', '*'], + ['__pyramid_scaffold/', '/pyramid_scaffold/*subpath', + 'pyramid:scaffolds/starter/+package+/static/', '*'], + ] + + for index, line in enumerate(L[2:]): + data = line.split() + self.assertEqual(data, expected[index]) + + def test_route_no_view(self): + from pyramid.renderers import null_renderer as nr + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b', request_method='POST') + + command = self._makeOne() + L = [] + command.out = L.append + command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = [ + 'foo', '/a/b', + '', + 'POST', + ] + self.assertEqual(compare_to, expected) + + def test_route_as_wsgiapp(self): + from pyramid.wsgi import wsgiapp2 + + config1 = self._makeConfig(autocommit=True) + def view1(context, request): return 'view1' + config1.add_route('foo', '/a/b', request_method='POST') + config1.add_view(view=view1, route_name='foo') + + config2 = self._makeConfig(autocommit=True) + config2.add_route('foo', '/a/b', request_method='POST') + config2.add_view( + wsgiapp2(config1.make_wsgi_app()), + route_name='foo', + ) + + command = self._makeOne() + L = [] + command.out = L.append + command.bootstrap = (dummy.DummyBootstrap(registry=config2.registry),) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = [ + 'foo', '/a/b', + '', + 'POST', + ] + self.assertEqual(compare_to, expected) + class Test_main(unittest.TestCase): def _callFUT(self, argv): from pyramid.scripts.proutes import main -- cgit v1.2.3 From e2274e5d1aa8e125c26a2e8d168a64e1b9e68db2 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 14 Dec 2014 20:27:26 -0800 Subject: Fix py26 support --- pyramid/scripts/proutes.py | 14 ++++++++++---- tox.ini | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index b9a33f5bc..d68f35cef 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -13,7 +13,6 @@ from pyramid.interfaces import ( from pyramid.scripts.common import parse_vars from pyramid.static import static_view from zope.interface import Interface -from collections import OrderedDict PAD = 3 @@ -117,14 +116,15 @@ def get_route_data(route, registry): ) route_request_methods = None - view_request_methods = OrderedDict() + view_request_methods_order = [] + view_request_methods = {} view_callable = None route_intr = registry.introspector.get( 'routes', route.name ) - if (request_iface is None) or (route.factory is not None): + if request_iface is None: return [ (route.name, _get_pattern(route), UNKNOWN_KEY, ANY_KEY) ] @@ -136,10 +136,12 @@ def get_route_data(route, registry): default=None ) view_module = _get_view_module(view_callable) + # Introspectables can be turned off, so there could be a chance # that we have no `route_intr` but we do have a route + callable if route_intr is None: view_request_methods[view_module] = [] + view_request_methods_order.append(view_module) else: route_request_methods = route_intr['request_methods'] @@ -155,6 +157,7 @@ def get_route_data(route, registry): if view_module not in view_request_methods: view_request_methods[view_module] = [] + view_request_methods_order.append(view_module) if isinstance(request_method, string_types): request_method = (request_method,) @@ -163,13 +166,16 @@ def get_route_data(route, registry): else: if view_module not in view_request_methods: view_request_methods[view_module] = [] + view_request_methods_order.append(view_module) else: view_request_methods[view_module] = [] + view_request_methods_order.append(view_module) final_routes = [] - for view_module, methods in view_request_methods.items(): + for view_module in view_request_methods_order: + methods = view_request_methods[view_module] request_methods = _get_request_methods( route_request_methods, methods diff --git a/tox.ini b/tox.ini index 714c5b6d3..0b77d588a 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ commands = [testenv:cover] basepython = - python2.6 + python2.7 commands = python setup.py dev python setup.py nosetests --with-xunit --with-xcoverage -- cgit v1.2.3 From 59ee9f5fe96f133ff3582dbf34ea7da43ef39029 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 14 Dec 2014 20:35:03 -0800 Subject: Switch tox back to 2.6, was supposed to be local --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 0b77d588a..714c5b6d3 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ commands = [testenv:cover] basepython = - python2.7 + python2.6 commands = python setup.py dev python setup.py nosetests --with-xunit --with-xcoverage -- cgit v1.2.3 From 228a5e5b3806c07c4d568ec491c4f83be5facbb1 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 14 Dec 2014 21:09:48 -0800 Subject: Added support for `not_` request_method checks --- pyramid/scripts/proutes.py | 28 ++++++++++++-- pyramid/tests/test_scripts/test_proutes.py | 60 ++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index d68f35cef..5d860b47d 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -9,6 +9,7 @@ from pyramid.interfaces import ( IViewClassifier, IView, ) +from pyramid.config import not_ from pyramid.scripts.common import parse_vars from pyramid.static import static_view @@ -44,6 +45,19 @@ def _get_print_format(max_name, max_pattern, max_view, max_method): def _get_request_methods(route_request_methods, view_request_methods): + excludes = set() + + if route_request_methods: + route_request_methods = set(route_request_methods) + + if view_request_methods: + view_request_methods = set(view_request_methods) + + for method in view_request_methods.copy(): + if method.startswith('!'): + view_request_methods.remove(method) + excludes.add(method[1:]) + has_route_methods = route_request_methods is not None has_view_methods = len(view_request_methods) > 0 has_methods = has_route_methods or has_view_methods @@ -55,14 +69,20 @@ def _get_request_methods(route_request_methods, view_request_methods): elif has_route_methods is True and has_view_methods is False: request_methods = route_request_methods else: - request_methods = set(route_request_methods).intersection( - set(view_request_methods) + request_methods = route_request_methods.intersection( + view_request_methods ) + request_methods = set(request_methods).difference(excludes) + if has_methods and not request_methods: request_methods = '' elif request_methods: - request_methods = ','.join(request_methods) + if excludes and request_methods == set([ANY_KEY]): + for exclude in excludes: + request_methods.add('!%s' % exclude) + + request_methods = ','.join(sorted(request_methods)) return request_methods @@ -161,6 +181,8 @@ def get_route_data(route, registry): if isinstance(request_method, string_types): request_method = (request_method,) + elif isinstance(request_method, not_): + request_method = ('!%s' % request_method.value,) view_request_methods[view_module].extend(request_method) else: diff --git a/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py index 0713f4ac9..f7a85bd1c 100644 --- a/pyramid/tests/test_scripts/test_proutes.py +++ b/pyramid/tests/test_scripts/test_proutes.py @@ -471,6 +471,66 @@ class TestPRoutesCommand(unittest.TestCase): ] self.assertEqual(compare_to, expected) + def test_route_is_get_view_request_method_not_post(self): + from pyramid.renderers import null_renderer as nr + from pyramid.config import not_ + + def view1(context, request): return 'view1' + + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b', request_method='GET') + config.add_view( + route_name='foo', + view=view1, + renderer=nr, + request_method=not_('POST') + ) + + command = self._makeOne() + L = [] + command.out = L.append + command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = [ + 'foo', '/a/b', + 'pyramid.tests.test_scripts.test_proutes.view1', + 'GET' + ] + self.assertEqual(compare_to, expected) + + def test_view_request_method_not_post(self): + from pyramid.renderers import null_renderer as nr + from pyramid.config import not_ + + def view1(context, request): return 'view1' + + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b') + config.add_view( + route_name='foo', + view=view1, + renderer=nr, + request_method=not_('POST') + ) + + command = self._makeOne() + L = [] + command.out = L.append + command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = [ + 'foo', '/a/b', + 'pyramid.tests.test_scripts.test_proutes.view1', + '!POST,*' + ] + self.assertEqual(compare_to, expected) + class Test_main(unittest.TestCase): def _callFUT(self, argv): from pyramid.scripts.proutes import main -- cgit v1.2.3 From 2711913daac645dc1960074d6c5121c8fb49b772 Mon Sep 17 00:00:00 2001 From: Adrian Teng Date: Wed, 17 Dec 2014 00:06:27 +0000 Subject: Add documentation on handling CORS pre-flights --- CONTRIBUTORS.txt | 2 ++ docs/narr/webob.rst | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 9c2191f3b..e4132cda5 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -236,3 +236,5 @@ Contributors - Randall Leeds, 2014/11/11 - Hugo Branquinho, 2014/11/25 + +- Adrian Teng, 2014/12/17 diff --git a/docs/narr/webob.rst b/docs/narr/webob.rst index 6a331e4bf..7d459a1f5 100644 --- a/docs/narr/webob.rst +++ b/docs/narr/webob.rst @@ -310,6 +310,10 @@ Python's ``urllib2`` instead of a Javascript AJAX request: req = urllib2.Request('http://localhost:6543/', json_payload, headers) resp = urllib2.urlopen(req) +If you are doing Cross-origin resource sharing (CORS), then the standard requires the browser to do a pre-flight HTTP OPTIONS request. The easiest way to handling this is adding an extra ``view_config`` for the same route, with ``request_method`` set to ``OPTIONS``, and setting the desired response header before returning. You can find examples of response headers here_. + +.. _here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests + .. index:: single: cleaning up after request -- cgit v1.2.3 From 023afb1fd5eedc3f8ba24d95cad1864d05eb0444 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 16 Dec 2014 19:53:58 -0500 Subject: repoze who docs moved --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 4bc8e2172..fa4578275 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -74,7 +74,7 @@ intersphinx_mapping = { 'http://docs.pylonsproject.org/projects/deform/en/latest', None), 'sqla': ('http://docs.sqlalchemy.org/en/latest', None), - 'who': ('http://docs.repoze.org/who/latest', None), + 'who': ('http://repozewho.readthedocs.org/en/latest', None), 'python': ('http://docs.python.org', None), 'python3': ('http://docs.python.org/3', None), 'tstring': -- cgit v1.2.3 From 2660f5053de5383aacf4ceff3d4e05d7e73f1635 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 16 Dec 2014 19:54:03 -0500 Subject: 79 cols --- docs/narr/webob.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/narr/webob.rst b/docs/narr/webob.rst index 7d459a1f5..0eb070b06 100644 --- a/docs/narr/webob.rst +++ b/docs/narr/webob.rst @@ -310,7 +310,11 @@ Python's ``urllib2`` instead of a Javascript AJAX request: req = urllib2.Request('http://localhost:6543/', json_payload, headers) resp = urllib2.urlopen(req) -If you are doing Cross-origin resource sharing (CORS), then the standard requires the browser to do a pre-flight HTTP OPTIONS request. The easiest way to handling this is adding an extra ``view_config`` for the same route, with ``request_method`` set to ``OPTIONS``, and setting the desired response header before returning. You can find examples of response headers here_. +If you are doing Cross-origin resource sharing (CORS), then the standard +requires the browser to do a pre-flight HTTP OPTIONS request. The easiest way +to handling this is adding an extra ``view_config`` for the same route, with +``request_method`` set to ``OPTIONS``, and setting the desired response header +before returning. You can find examples of response headers here_. .. _here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests -- cgit v1.2.3 From 85e432c622f2fe719c08ef932b66977f22f7cc90 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Thu, 18 Dec 2014 21:27:56 -0500 Subject: Speed up Travis start via 'sudo: false'. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 5a205b268..cb98fddbe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ # Wire up travis language: python +sudo: false env: - TOXENV=py26 -- cgit v1.2.3 From 0fa3ddf54b1324a8bcbf26b0cc7df8a06afe7c4f Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 23 Dec 2014 01:09:16 -0800 Subject: - remove pyramid_router.png because an .svg version is available per @tseaver --- docs/_static/pyramid_router.png | Bin 120643 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/_static/pyramid_router.png diff --git a/docs/_static/pyramid_router.png b/docs/_static/pyramid_router.png deleted file mode 100644 index 3c9f81158..000000000 Binary files a/docs/_static/pyramid_router.png and /dev/null differ -- cgit v1.2.3 From cac2128fcc71ad2e0c9b9f22046dc47adb92dfd0 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 23 Dec 2014 03:23:19 -0800 Subject: - add an index to the API directory for better SEO --- docs/api/index.rst | 12 ++++++++++++ docs/index.rst | 3 +-- 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 docs/api/index.rst diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 000000000..cb38aa0b2 --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,12 @@ +.. _html_api_documentation: + +API Documentation +================= + +Comprehensive reference material for every public API exposed by :app:`Pyramid`: + +.. toctree:: + :maxdepth: 1 + :glob: + + * diff --git a/docs/index.rst b/docs/index.rst index ac16ff237..fc7560e8f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -135,8 +135,6 @@ platforms. tutorials/wiki/index.rst tutorials/modwsgi/index.rst -.. _html_api_documentation: - API Documentation ================= @@ -146,6 +144,7 @@ Comprehensive reference material for every public API exposed by :app:`Pyramid`: :maxdepth: 1 :glob: + api/index api/* Change History -- cgit v1.2.3 From 1264707de3f9fa2d15943da3bf321b1304556aa3 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 23 Dec 2014 14:58:13 -0800 Subject: Add a --glob option to limit output E.g.: $ proutes development.ini --glob='user*' Name Pattern View Method ---- ------- ---- ------ user /profilesvc/v1/user/{user_id} pyramid.config.views. * user /profilesvc/v1/user/{user_id} cornice.service.wrapper GET,HEAD --- pyramid/scripts/proutes.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index 5d860b47d..fce90c0c1 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -1,3 +1,4 @@ +import fnmatch import optparse import sys import textwrap @@ -234,6 +235,9 @@ class PRoutesCommand(object): usage, description=textwrap.dedent(description) ) + parser.add_option('-g', '--glob', + action='store', type='string', dest='glob', + default='', help='Display routes matching glob pattern') def __init__(self, argv, quiet=False): self.options, self.args = self.parser.parse_args(argv[1:]) @@ -280,6 +284,12 @@ class PRoutesCommand(object): route_data = get_route_data(route, registry) for name, pattern, view, method in route_data: + if self.options.glob: + match = (fnmatch.fnmatch(name, self.options.glob) or + fnmatch.fnmatch(pattern, self.options.glob)) + if not match: + continue + if len(name) > max_name: max_name = len(name) -- cgit v1.2.3 From c966a9283c2c6515848e8a5f4f7c34e0fba733a8 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 25 Dec 2014 19:36:18 -0800 Subject: Added test for the view glob --- pyramid/tests/test_scripts/test_proutes.py | 41 ++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py index f7a85bd1c..4622383be 100644 --- a/pyramid/tests/test_scripts/test_proutes.py +++ b/pyramid/tests/test_scripts/test_proutes.py @@ -531,6 +531,47 @@ class TestPRoutesCommand(unittest.TestCase): ] self.assertEqual(compare_to, expected) + def test_view_glob(self): + from pyramid.renderers import null_renderer as nr + from pyramid.config import not_ + + def view1(context, request): return 'view1' + def view2(context, request): return 'view2' + + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b') + config.add_view( + route_name='foo', + view=view1, + renderer=nr, + request_method=not_('POST') + ) + + config.add_route('bar', '/b/a') + config.add_view( + route_name='bar', + view=view2, + renderer=nr, + request_method=not_('POST') + ) + + command = self._makeOne() + command.options.glob = '*foo*' + + L = [] + command.out = L.append + command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = [ + 'foo', '/a/b', + 'pyramid.tests.test_scripts.test_proutes.view1', + '!POST,*' + ] + self.assertEqual(compare_to, expected) + class Test_main(unittest.TestCase): def _callFUT(self, argv): from pyramid.scripts.proutes import main -- cgit v1.2.3 From a99d2d7e17f4c87d9307215a2ad8d583e74c3a50 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 25 Dec 2014 20:49:00 -0800 Subject: Add the --format flag --- pyramid/scripts/proutes.py | 106 ++++++++++++++++++++++++----- pyramid/tests/test_scripts/test_proutes.py | 94 +++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 17 deletions(-) diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index fce90c0c1..917d9af71 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -4,7 +4,7 @@ import sys import textwrap from pyramid.paster import bootstrap -from pyramid.compat import string_types +from pyramid.compat import (string_types, configparser) from pyramid.interfaces import ( IRouteRequest, IViewClassifier, @@ -35,14 +35,22 @@ def _get_pattern(route): return pattern -def _get_print_format(max_name, max_pattern, max_view, max_method): - fmt = '%-{0}s %-{1}s %-{2}s %-{3}s'.format( - max_name + PAD, - max_pattern + PAD, - max_view + PAD, - max_method + PAD, - ) - return fmt +def _get_print_format(fmt, max_name, max_pattern, max_view, max_method): + print_fmt = '' + max_map = { + 'name': max_name, + 'pattern': max_pattern, + 'view': max_view, + 'method': max_method, + } + sizes = [] + + for index, col in enumerate(fmt): + size = max_map[col] + PAD + print_fmt += '{{%s: <{%s}}} ' % (col, index) + sizes.append(size) + + return print_fmt.format(*sizes) def _get_request_methods(route_request_methods, view_request_methods): @@ -230,7 +238,7 @@ class PRoutesCommand(object): bootstrap = (bootstrap,) stdout = sys.stdout usage = '%prog config_uri' - + ConfigParser = configparser.ConfigParser # testing parser = optparse.OptionParser( usage, description=textwrap.dedent(description) @@ -239,9 +247,49 @@ class PRoutesCommand(object): action='store', type='string', dest='glob', default='', help='Display routes matching glob pattern') + parser.add_option('-f', '--format', + action='store', type='string', dest='format', + default='', help=('Choose which columns to display, this ' + 'will override the format key in the ' + '[proutes] ini section')) + def __init__(self, argv, quiet=False): self.options, self.args = self.parser.parse_args(argv[1:]) self.quiet = quiet + self.available_formats = [ + 'name', 'pattern', 'view', 'method' + ] + self.column_format = self.available_formats + + def validate_formats(self, formats): + invalid_formats = [] + for fmt in formats: + if fmt not in self.available_formats: + invalid_formats.append(fmt) + + msg = ( + 'You provided invalid formats %s, ' + 'Available formats are %s' + ) + + if invalid_formats: + msg = msg % (invalid_formats, self.available_formats) + self.out(msg) + return False + + return True + + def proutes_file_config(self, filename): + config = self.ConfigParser() + config.read(filename) + try: + items = config.items('proutes') + for k, v in items: + if 'format' == k: + self.column_format = [x.strip() for x in v.split('\n')] + + except configparser.NoSectionError: + return def out(self, msg): # pragma: no cover if not self.quiet: @@ -261,6 +309,16 @@ class PRoutesCommand(object): env = self.bootstrap[0](config_uri, options=parse_vars(self.args[1:])) registry = env['registry'] mapper = self._get_mapper(registry) + self.proutes_file_config(config_uri) + + if self.options.format: + columns = self.options.format.split(',') + self.column_format = [x.strip() for x in columns] + + is_valid = self.validate_formats(self.column_format) + + if is_valid is False: + return 2 if mapper is None: return 0 @@ -275,10 +333,17 @@ class PRoutesCommand(object): if len(routes) == 0: return 0 - mapped_routes = [ - ('Name', 'Pattern', 'View', 'Method'), - ('----', '-------', '----', '------') - ] + mapped_routes = [{ + 'name': 'Name', + 'pattern': 'Pattern', + 'view': 'View', + 'method': 'Method' + },{ + 'name': '----', + 'pattern': '-------', + 'view': '----', + 'method': '------' + }] for route in routes: route_data = get_route_data(route, registry) @@ -302,12 +367,19 @@ class PRoutesCommand(object): if len(method) > max_method: max_method = len(method) - mapped_routes.append((name, pattern, view, method)) + mapped_routes.append({ + 'name': name, + 'pattern': pattern, + 'view': view, + 'method': method + }) - fmt = _get_print_format(max_name, max_pattern, max_view, max_method) + fmt = _get_print_format( + self.column_format, max_name, max_pattern, max_view, max_method + ) for route in mapped_routes: - self.out(fmt % route) + self.out(fmt.format(**route)) return 0 diff --git a/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py index 4622383be..d51baaa01 100644 --- a/pyramid/tests/test_scripts/test_proutes.py +++ b/pyramid/tests/test_scripts/test_proutes.py @@ -20,6 +20,7 @@ class TestPRoutesCommand(unittest.TestCase): cmd = self._getTargetClass()([]) cmd.bootstrap = (dummy.DummyBootstrap(),) cmd.args = ('/foo/bar/myapp.ini#myapp',) + return cmd def _makeRegistry(self): @@ -572,6 +573,99 @@ class TestPRoutesCommand(unittest.TestCase): ] self.assertEqual(compare_to, expected) + def test_good_format(self): + from pyramid.renderers import null_renderer as nr + from pyramid.config import not_ + + def view1(context, request): return 'view1' + + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b') + config.add_view( + route_name='foo', + view=view1, + renderer=nr, + request_method=not_('POST') + ) + + command = self._makeOne() + command.options.glob = '*foo*' + command.options.format = 'method,name' + L = [] + command.out = L.append + command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = ['!POST,*', 'foo'] + + self.assertEqual(compare_to, expected) + self.assertEqual(L[0].split(), ['Method', 'Name']) + + def test_bad_format(self): + from pyramid.renderers import null_renderer as nr + from pyramid.config import not_ + + def view1(context, request): return 'view1' + + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b') + config.add_view( + route_name='foo', + view=view1, + renderer=nr, + request_method=not_('POST') + ) + + command = self._makeOne() + command.options.glob = '*foo*' + command.options.format = 'predicates,name,pattern' + L = [] + command.out = L.append + command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + expected = ( + "You provided invalid formats ['predicates'], " + "Available formats are ['name', 'pattern', 'view', 'method']" + ) + result = command.run() + self.assertEqual(result, 2) + self.assertEqual(L[0], expected) + + def test_config_format_ini(self): + from pyramid.renderers import null_renderer as nr + from pyramid.config import not_ + + def view1(context, request): return 'view1' + + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b') + config.add_view( + route_name='foo', + view=view1, + renderer=nr, + request_method=not_('POST') + ) + + command = self._makeOne() + command.options.glob = '*foo*' + command.options.format = 'method,name' + L = [] + command.out = L.append + command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + config_factory = dummy.DummyConfigParserFactory() + command.ConfigParser = config_factory + config_factory.items = [('format', 'method\name')] + + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = ['!POST,*', 'foo'] + + self.assertEqual(compare_to, expected) + self.assertEqual(L[0].split(), ['Method', 'Name']) + class Test_main(unittest.TestCase): def _callFUT(self, argv): from pyramid.scripts.proutes import main -- cgit v1.2.3 From bc26debd9ed2a46fca1b0931c78b4054bd37841d Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 25 Dec 2014 23:42:05 -0800 Subject: Add support for passing unbound class methods to `add_view` --- pyramid/config/views.py | 13 ++++++++++++- pyramid/tests/test_config/test_views.py | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index c01b72e12..3e305055f 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -42,7 +42,8 @@ from pyramid.compat import ( url_quote, WIN, is_bound_method, - is_nonstr_iter + is_nonstr_iter, + im_self, ) from pyramid.exceptions import ( @@ -418,6 +419,16 @@ class DefaultViewMapper(object): self.attr = kw.get('attr') def __call__(self, view): + # Map the attr directly if the passed in view is method and a + # constructor is defined and must be unbound (for backwards + # compatibility) + if inspect.ismethod(view): + is_bound = getattr(view, im_self, None) is not None + + if not is_bound: + self.attr = view.__name__ + view = view.im_class + if inspect.isclass(view): view = self.map_class(view) else: diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index b0d03fb72..664208fad 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1666,6 +1666,27 @@ class TestViewsConfigurationMixin(unittest.TestCase): renderer=null_renderer) self.assertRaises(ConfigurationConflictError, config.commit) + def test_add_view_class_method_no_attr(self): + from pyramid.renderers import null_renderer + from zope.interface import directlyProvides + + class ViewClass(object): + def __init__(self, request): + self.request = request + + def run(self): + return 'OK' + + config = self._makeOne(autocommit=True) + config.add_view(view=ViewClass.run, renderer=null_renderer) + + wrapper = self._getViewCallable(config) + context = DummyContext() + directlyProvides(context, IDummy) + request = self._makeRequest(config) + result = wrapper(context, request) + self.assertEqual(result, 'OK') + def test_derive_view_function(self): from pyramid.renderers import null_renderer def view(request): -- cgit v1.2.3 From 88c11a8f09d6e9749a705cb62caa945c496e84e9 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 26 Dec 2014 00:30:25 -0800 Subject: Refactored how `ResponseClass` is used so it can be overridden --- pyramid/renderers.py | 7 +++---- pyramid/request.py | 10 +++++----- pyramid/util.py | 21 +++++++++++++++++++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index e647ebacf..3d5390840 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -10,7 +10,6 @@ from zope.interface.registry import Components from pyramid.interfaces import ( IJSONAdapter, IRendererFactory, - IResponseFactory, IRendererInfo, ) @@ -19,6 +18,8 @@ from pyramid.compat import ( text_type, ) +from pyramid.util import _get_response_factory + from pyramid.decorator import reify from pyramid.events import BeforeRender @@ -448,9 +449,7 @@ class RendererHelper(object): if response is None: # request is None or request is not a pyramid.response.Response registry = self.registry - response_factory = registry.queryUtility(IResponseFactory, - default=Response) - + response_factory = _get_response_factory(registry, request) response = response_factory() if result is not None: diff --git a/pyramid/request.py b/pyramid/request.py index bc2889310..b66b8926c 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -10,7 +10,6 @@ from pyramid.interfaces import ( IRequest, IResponse, ISessionFactory, - IResponseFactory, ) from pyramid.compat import ( @@ -27,7 +26,10 @@ from pyramid.security import ( AuthorizationAPIMixin, ) from pyramid.url import URLMethodsMixin -from pyramid.util import InstancePropertyMixin +from pyramid.util import ( + InstancePropertyMixin, + _get_response_factory, +) class TemplateContext(object): pass @@ -214,9 +216,7 @@ class Request( right" attributes (e.g. by calling ``request.response.set_cookie()``) within a view that uses a renderer. Mutations to this response object will be preserved in the response sent to the client.""" - registry = self.registry - response_factory = registry.queryUtility(IResponseFactory, - default=Response) + response_factory = _get_response_factory(self.registry, self) return response_factory() def is_response(self, ob): diff --git a/pyramid/util.py b/pyramid/util.py index 6de53d559..ee642164a 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -15,6 +15,10 @@ from pyramid.exceptions import ( CyclicDependencyError, ) +from pyramid.interfaces import ( + IResponseFactory, + ) + from pyramid.compat import ( iteritems_, is_nonstr_iter, @@ -25,6 +29,7 @@ from pyramid.compat import ( ) from pyramid.interfaces import IActionInfo +from pyramid.response import Response from pyramid.path import DottedNameResolver as _DottedNameResolver class DottedNameResolver(_DottedNameResolver): @@ -551,3 +556,19 @@ def action_method(wrapped): wrapper.__docobj__ = wrapped return wrapper +def _get_response_factory(registry, request=None): + """ Obtain a :class: `pyramid.response.Response` using the + ``request.ResponseClass`` property if available. + """ + # Request is `None` or does not have a `ResponseClass` + if hasattr(request, 'ResponseClass'): + response_class = request.ResponseClass + else: + response_class = Response + + response_factory = registry.queryUtility( + IResponseFactory, + default=response_class + ) + + return response_factory -- cgit v1.2.3 From 367ffec92bd502075e0f2f827f4b9d9b3caea748 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 26 Dec 2014 00:44:44 -0800 Subject: Add test to show usage of custom response class --- pyramid/tests/test_config/test_init.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 1e58e4d0f..c9eaf7c27 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -546,6 +546,36 @@ class ConfiguratorTests(unittest.TestCase): utility = reg.getUtility(IRequestFactory) self.assertEqual(utility, factory) + def test_setup_registry_request_factory_custom_response_class(self): + from pyramid.registry import Registry + from pyramid.interfaces import IRequestFactory + from pyramid.request import Request + + class MyResponse(object): + pass + + class MyRequest(Request): + ResponseClass = MyResponse + + reg = Registry() + config = self._makeOne(reg) + factory = MyRequest({ + 'PATH_INFO': '/', + 'wsgi.url_scheme': 'http', + 'HTTP_HOST': 'localhost', + 'SERVER_PROTOCOL': '1.0', + }) + factory.registry = reg + + config.setup_registry(request_factory=factory) + self.assertEqual(reg.queryUtility(IRequestFactory), None) + config.commit() + utility = reg.getUtility(IRequestFactory) + self.assertEqual(utility, factory) + + new_response = factory.response + self.assertTrue(isinstance(new_response, MyResponse)) + def test_setup_registry_request_factory_dottedname(self): from pyramid.registry import Registry from pyramid.interfaces import IRequestFactory -- cgit v1.2.3 From 8c54a34d5d3211537b8705b04ae8662274641828 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 26 Dec 2014 02:32:58 -0800 Subject: - adding back .png file because PDF cannot include and build SVG files. Also renamed images to use a 'imagename.*' wildcard so that the correct format is chosen. See http://stackoverflow.com/questions/6473660/using-sphinx-docs-how-can-i-specify-png-image-formats-for-html-builds-and-pdf-im --- docs/_static/pyramid_router.png | Bin 0 -> 120643 bytes docs/narr/router.rst | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 docs/_static/pyramid_router.png diff --git a/docs/_static/pyramid_router.png b/docs/_static/pyramid_router.png new file mode 100644 index 000000000..3c9f81158 Binary files /dev/null and b/docs/_static/pyramid_router.png differ diff --git a/docs/narr/router.rst b/docs/narr/router.rst index 693217a6b..6f90c70cc 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -9,7 +9,7 @@ Request Processing ================== -.. image:: ../_static/pyramid_request_processing.svg +.. image:: ../_static/pyramid_request_processing.* :alt: Request Processing Once a :app:`Pyramid` application is up and running, it is ready to accept @@ -119,7 +119,7 @@ request enters a :app:`Pyramid` application through to the point that #. The :term:`thread local` stack is popped. -.. image:: ../_static/pyramid_router.svg +.. image:: ../_static/pyramid_router.* :alt: Pyramid Router This is a very high-level overview that leaves out various details. For more -- cgit v1.2.3 From 432e61ed8e0174850ddfeefc81fb31c27a018fa7 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 26 Dec 2014 02:49:01 -0800 Subject: - establish minimum version of 1.2.3 for Sphinx. I hope this works. See https://github.com/Pylons/pyramid/issues/1068 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d736dc38d..629b77f3d 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ if not PY3: tests_require.append('zope.component>=3.11.0') docs_extras = [ - 'Sphinx', + 'Sphinx >= 1.2.3', 'docutils', 'repoze.sphinx.autointerface', ] -- cgit v1.2.3 From c0a01d1a49c9a02b1aa7266301fd64fb679e1d53 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 26 Dec 2014 04:17:42 -0800 Subject: Added 2 more tests directly to the util function --- pyramid/tests/test_util.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index a18fa8d16..c9c48aede 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -619,6 +619,33 @@ class TestActionInfo(unittest.TestCase): "Line 0 of file filename:\n linerepr ") +class TestGetResponseFactory(unittest.TestCase): + def test_no_request(self): + from pyramid.util import _get_response_factory + from pyramid.registry import Registry + from pyramid.response import Response + + registry = Registry() + factory = _get_response_factory(registry) + self.assertEqual(factory, Response) + + def test_with_request(self): + from pyramid.util import _get_response_factory + from pyramid.registry import Registry + from pyramid.request import Request + + class MyResponse(object): + pass + + class MyRequest(Request): + ResponseClass = MyResponse + registry = Registry() + + request = MyRequest({}) + factory = _get_response_factory(registry, request) + self.assertEqual(factory, MyResponse) + + def dummyfunc(): pass class Dummy(object): -- cgit v1.2.3 From 582c2ed180120d07c825e4897350f5d1a6285afa Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 26 Dec 2014 05:06:33 -0800 Subject: Add support for static routes --- pyramid/config/routes.py | 6 ++++++ pyramid/scripts/proutes.py | 9 +++++++-- pyramid/tests/test_scripts/dummy.py | 2 +- pyramid/tests/test_scripts/test_proutes.py | 20 ++++++++++++++++++++ pyramid/urldispatch.py | 11 ++++++++++- 5 files changed, 44 insertions(+), 4 deletions(-) diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index f1463b50b..509955cdd 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -303,6 +303,8 @@ class RoutesConfiguratorMixin(object): # check for an external route; an external route is one which is # is a full url (e.g. 'http://example.com/{id}') parsed = urlparse.urlparse(pattern) + external_url = pattern + if parsed.hostname: pattern = parsed.path @@ -357,6 +359,10 @@ class RoutesConfiguratorMixin(object): intr['pregenerator'] = pregenerator intr['static'] = static intr['use_global_views'] = use_global_views + + if static is True: + intr['external_url'] = external_url + introspectables.append(intr) if factory: diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index 917d9af71..2155b4983 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -172,8 +172,13 @@ def get_route_data(route, registry): view_request_methods[view_module] = [] view_request_methods_order.append(view_module) else: - route_request_methods = route_intr['request_methods'] + if route_intr.get('static', False) is True: + return [ + (route.name, route_intr['external_url'], UNKNOWN_KEY, ANY_KEY) + ] + + route_request_methods = route_intr['request_methods'] view_intr = registry.introspector.related(route_intr) if view_intr: @@ -328,7 +333,7 @@ class PRoutesCommand(object): max_view = len('View') max_method = len('Method') - routes = mapper.get_routes() + routes = mapper.get_routes(include_static=True) if len(routes) == 0: return 0 diff --git a/pyramid/tests/test_scripts/dummy.py b/pyramid/tests/test_scripts/dummy.py index 366aa00b5..930b9ed64 100644 --- a/pyramid/tests/test_scripts/dummy.py +++ b/pyramid/tests/test_scripts/dummy.py @@ -60,7 +60,7 @@ class DummyMapper(object): def __init__(self, *routes): self.routes = routes - def get_routes(self): + def get_routes(self, include_static=False): return self.routes class DummyRoute(object): diff --git a/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py index d51baaa01..446d772ff 100644 --- a/pyramid/tests/test_scripts/test_proutes.py +++ b/pyramid/tests/test_scripts/test_proutes.py @@ -666,6 +666,26 @@ class TestPRoutesCommand(unittest.TestCase): self.assertEqual(compare_to, expected) self.assertEqual(L[0].split(), ['Method', 'Name']) + def test_static_routes_included_in_list(self): + from pyramid.renderers import null_renderer as nr + + config = self._makeConfig(autocommit=True) + config.add_route('foo', 'http://example.com/bar.aspx', static=True) + + command = self._makeOne() + L = [] + command.out = L.append + command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = [ + 'foo', 'http://example.com/bar.aspx', + '', '*', + ] + self.assertEqual(compare_to, expected) + class Test_main(unittest.TestCase): def _callFUT(self, argv): from pyramid.scripts.proutes import main diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py index fe4d433c3..349742c4a 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -42,12 +42,17 @@ class Route(object): class RoutesMapper(object): def __init__(self): self.routelist = [] + self.static_routes = [] + self.routes = {} def has_routes(self): return bool(self.routelist) - def get_routes(self): + def get_routes(self, include_static=False): + if include_static is True: + return self.routelist + self.static_routes + return self.routelist def get_route(self, name): @@ -59,9 +64,13 @@ class RoutesMapper(object): oldroute = self.routes[name] if oldroute in self.routelist: self.routelist.remove(oldroute) + route = Route(name, pattern, factory, predicates, pregenerator) if not static: self.routelist.append(route) + else: + self.static_routes.append(route) + self.routes[name] = route return route -- cgit v1.2.3 From e21662924a296f391fdbbf725fb79e409bca59f4 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 26 Dec 2014 05:11:51 -0800 Subject: pass proper registry --- pyramid/tests/test_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index c9c48aede..0ebbd80a2 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -642,7 +642,7 @@ class TestGetResponseFactory(unittest.TestCase): registry = Registry() request = MyRequest({}) - factory = _get_response_factory(registry, request) + factory = _get_response_factory(request.registry, request) self.assertEqual(factory, MyResponse) -- cgit v1.2.3 From cb5afac9c9539967591a46f50efdf6ee0d207fbf Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 26 Dec 2014 19:59:43 -0800 Subject: - pin Sphinx >= 1.2.3 for RTD --- rtd.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtd.txt b/rtd.txt index b449ac73c..4aecd9933 100644 --- a/rtd.txt +++ b/rtd.txt @@ -1,4 +1,4 @@ +Sphinx >= 1.2.3 repoze.sphinx.autointerface repoze.lru pylons_sphinx_latesturl - -- cgit v1.2.3 From 2253647075ace9e99171f3e227f5debbcafdd8b8 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 10:46:57 -0600 Subject: first cut at a re-entrant configurator where tests still pass --- pyramid/config/__init__.py | 67 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index cfa35ec6c..83683daeb 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -975,7 +975,7 @@ class Configurator( class ActionState(object): def __init__(self): # NB "actions" is an API, dep'd upon by pyramid_zcml's load_zcml func - self.actions = [] + self.actions = [] self._seen_files = set() def processSpec(self, spec): @@ -1059,10 +1059,54 @@ class ActionState(object): >>> output [('f', (1,), {}), ('f', (2,), {})] - """ + The execution is re-entrant such that actions may be added by other + actions with the one caveat that the order of any added actions must + be equal to or larger than the current action. + + >>> output = [] + >>> def f(*a, **k): + ... output.append(('f', a, k)) + ... context.actions.append((3, g, (8,), {})) + >>> def g(*a, **k): + ... output.append(('g', a, k)) + >>> context.actions = [ + ... (1, f, (1,)), + ... (2, f, (2,)), + ... ] + >>> context.execute_actions() + >>> output + [('f', (1,), {}), ('f', (2,), {}), ('g', (8,), {})] + """ try: - for action in resolveConflicts(self.actions): + all_actions = self.actions + self.actions = [] + executed_actions = [] + + # resolve the new action list against what we have already + # executed -- if a new action appears intertwined in the list + # of already-executed actions then someone wrote a broken + # re-entrant action because it scheduled the action *after* it + # should have been executed (as defined by the action order) + def resume(actions): + for a, b in itertools.izip_longest(actions, executed_actions): + if b is None and a is not None: + # common case is that we are executing every action + yield a + elif b is not None and a != b: + raise RuntimeError('Re-entrant failure - attempted ' + 'to resolve actions in a different ' + 'order from the active execution ' + 'path.') + else: + # resolved action is in the same location as before, + # so we are in good shape, but the action is already + # executed so we skip it + assert b is not None and a == b + + pending_actions = resume(resolveConflicts(all_actions)) + action = next(pending_actions, None) + while action is not None: callable = action['callable'] args = action['args'] kw = action['kw'] @@ -1088,10 +1132,25 @@ class ActionState(object): if introspector is not None: for introspectable in introspectables: introspectable.register(introspector, info) - + + executed_actions.append(action) + + # We cleared the actions list prior to execution so if there + # are some new actions then we add them to the mix and resolve + # conflicts again. This orders the new actions as well as + # ensures that the previously executed actions have no new + # conflicts. + if self.actions: + all_actions.extend(self.actions) + self.actions = [] + pending_actions = resume(resolveConflicts(all_actions)) + action = next(pending_actions, None) + finally: if clear: del self.actions[:] + else: + self.actions = all_actions # this function is licensed under the ZPL (stolen from Zope) def resolveConflicts(actions): -- cgit v1.2.3 From 5d2302a2b8d968245a123e54a8f01cd62c97cf69 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 15:57:08 -0600 Subject: izip_longest is not valid on py3 --- pyramid/compat.py | 4 ++++ pyramid/config/__init__.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pyramid/compat.py b/pyramid/compat.py index bfa345b88..301984749 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -244,3 +244,7 @@ else: def is_bound_method(ob): return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None +if PY3: # pragma: no cover + from itertools import zip_longest +else: + from itertools import izip_longest as zip_longest diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 83683daeb..e907cbb14 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -23,6 +23,7 @@ from pyramid.compat import ( text_, reraise, string_types, + zip_longest, ) from pyramid.events import ApplicationCreated @@ -1089,7 +1090,7 @@ class ActionState(object): # re-entrant action because it scheduled the action *after* it # should have been executed (as defined by the action order) def resume(actions): - for a, b in itertools.izip_longest(actions, executed_actions): + for a, b in zip_longest(actions, executed_actions): if b is None and a is not None: # common case is that we are executing every action yield a -- cgit v1.2.3 From a52326b00b843b94b569d35a8d91a2a4c78b56a0 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 15:57:26 -0600 Subject: refactor loop to combine conflict resolution paths into one --- pyramid/config/__init__.py | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index e907cbb14..740c9c47d 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1080,9 +1080,9 @@ class ActionState(object): """ try: - all_actions = self.actions - self.actions = [] + all_actions = [] executed_actions = [] + pending_actions = iter([]) # resolve the new action list against what we have already # executed -- if a new action appears intertwined in the list @@ -1095,19 +1095,32 @@ class ActionState(object): # common case is that we are executing every action yield a elif b is not None and a != b: - raise RuntimeError('Re-entrant failure - attempted ' - 'to resolve actions in a different ' - 'order from the active execution ' - 'path.') + raise ConfigurationError( + 'Re-entrant failure - attempted to resolve ' + 'actions in a different order from the active ' + 'execution path.') else: # resolved action is in the same location as before, # so we are in good shape, but the action is already # executed so we skip it assert b is not None and a == b - pending_actions = resume(resolveConflicts(all_actions)) - action = next(pending_actions, None) - while action is not None: + while True: + # We clear the actions list prior to execution so if there + # are some new actions then we add them to the mix and resolve + # conflicts again. This orders the new actions as well as + # ensures that the previously executed actions have no new + # conflicts. + if self.actions: + all_actions.extend(self.actions) + self.actions = [] + pending_actions = resume(resolveConflicts(all_actions)) + + action = next(pending_actions, None) + if action is None: + # we are done! + break + callable = action['callable'] args = action['args'] kw = action['kw'] @@ -1128,7 +1141,7 @@ class ActionState(object): ConfigurationExecutionError(t, v, info), tb) finally: - del t, v, tb + del t, v, tb if introspector is not None: for introspectable in introspectables: @@ -1136,17 +1149,6 @@ class ActionState(object): executed_actions.append(action) - # We cleared the actions list prior to execution so if there - # are some new actions then we add them to the mix and resolve - # conflicts again. This orders the new actions as well as - # ensures that the previously executed actions have no new - # conflicts. - if self.actions: - all_actions.extend(self.actions) - self.actions = [] - pending_actions = resume(resolveConflicts(all_actions)) - action = next(pending_actions, None) - finally: if clear: del self.actions[:] -- cgit v1.2.3 From a1a5306b89bc652ad089551f0976a8b5f68d6b63 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 15:58:21 -0600 Subject: optimize the conflict resolution to occur against only executed actions --- pyramid/config/__init__.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 740c9c47d..1a9cc3f5a 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1112,9 +1112,20 @@ class ActionState(object): # ensures that the previously executed actions have no new # conflicts. if self.actions: + # Only resolve the new actions against executed_actions + # instead of everything to avoid redundant checks. + # Assume ``actions = resolveConflicts([A, B, C])`` which + # after conflict checks, resulted in ``actions == [A]`` + # then we know action A won out or a conflict would have + # been raised. Thus, when action D is added later, we only + # need to check the new action against A. + # ``actions = resolveConflicts([A, D]) should drop the + # number of redundant checks down from O(n^2) closer to + # O(n lg n). + pending_actions = resume(resolveConflicts( + executed_actions + self.actions)) all_actions.extend(self.actions) self.actions = [] - pending_actions = resume(resolveConflicts(all_actions)) action = next(pending_actions, None) if action is None: -- cgit v1.2.3 From d643c10413d49d5ec9c2bc0d6dc2dc4fb08c99c9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 16:05:20 -0600 Subject: improve error output a bit --- pyramid/config/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 1a9cc3f5a..0bd61bc39 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1096,9 +1096,10 @@ class ActionState(object): yield a elif b is not None and a != b: raise ConfigurationError( - 'Re-entrant failure - attempted to resolve ' - 'actions in a different order from the active ' - 'execution path.') + 'During execution a re-entrant action was added ' + 'that modified the planned execution order in a ' + 'way that is incompatible with what has already ' + 'been done.') else: # resolved action is in the same location as before, # so we are in good shape, but the action is already -- cgit v1.2.3 From cf6e03bf042483283c2a7a51fec29a6d73887965 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 26 Dec 2014 22:59:43 -0600 Subject: modify text --- pyramid/config/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 0bd61bc39..c35338826 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1099,7 +1099,7 @@ class ActionState(object): 'During execution a re-entrant action was added ' 'that modified the planned execution order in a ' 'way that is incompatible with what has already ' - 'been done.') + 'been executed.') else: # resolved action is in the same location as before, # so we are in good shape, but the action is already -- cgit v1.2.3 From d579409e656df9f92a89000d66e60ec71b5857bc Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 26 Dec 2014 21:35:06 -0800 Subject: - update theme with new image --- docs/_themes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_themes b/docs/_themes index 3bec9280a..b14bf8c2a 160000 --- a/docs/_themes +++ b/docs/_themes @@ -1 +1 @@ -Subproject commit 3bec9280a6cedb15e97e5899021aa8d723c25388 +Subproject commit b14bf8c2a0d95ae8e3d38d07ad3721370ae6f3f8 -- cgit v1.2.3 From 873fa0483a7bfeafa5590b6d992ac52228d1b509 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 27 Dec 2014 00:10:03 -0600 Subject: add reentrant tests --- pyramid/config/__init__.py | 11 ++++++---- pyramid/tests/test_config/test_init.py | 39 ++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index c35338826..e81ccee3f 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1072,7 +1072,6 @@ class ActionState(object): ... output.append(('g', a, k)) >>> context.actions = [ ... (1, f, (1,)), - ... (2, f, (2,)), ... ] >>> context.execute_actions() >>> output @@ -1114,7 +1113,8 @@ class ActionState(object): # conflicts. if self.actions: # Only resolve the new actions against executed_actions - # instead of everything to avoid redundant checks. + # and pending_actions instead of everything to avoid + # redundant checks. # Assume ``actions = resolveConflicts([A, B, C])`` which # after conflict checks, resulted in ``actions == [A]`` # then we know action A won out or a conflict would have @@ -1123,9 +1123,12 @@ class ActionState(object): # ``actions = resolveConflicts([A, D]) should drop the # number of redundant checks down from O(n^2) closer to # O(n lg n). - pending_actions = resume(resolveConflicts( - executed_actions + self.actions)) all_actions.extend(self.actions) + pending_actions = resume(resolveConflicts( + executed_actions + + list(pending_actions) + + self.actions + )) self.actions = [] action = next(pending_actions, None) diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 1e58e4d0f..40cc83885 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -1503,6 +1503,45 @@ class TestActionState(unittest.TestCase): self.assertRaises(ConfigurationExecutionError, c.execute_actions) self.assertEqual(output, [('f', (1,), {}), ('f', (2,), {})]) + def test_reentrant_action(self): + output = [] + c = self._makeOne() + def f(*a, **k): + output.append(('f', a, k)) + c.actions.append((3, g, (8,), {})) + def g(*a, **k): + output.append(('g', a, k)) + c.actions = [ + (1, f, (1,)), + ] + c.execute_actions() + self.assertEqual(output, [('f', (1,), {}), ('g', (8,), {})]) + + def test_reentrant_action_error(self): + from pyramid.exceptions import ConfigurationError + c = self._makeOne() + def f(*a, **k): + c.actions.append((3, g, (8,), {}, (), None, -1)) + def g(*a, **k): pass + c.actions = [ + (1, f, (1,)), + ] + self.assertRaises(ConfigurationError, c.execute_actions) + + def test_reentrant_action_without_clear(self): + c = self._makeOne() + def f(*a, **k): + c.actions.append((3, g, (8,))) + def g(*a, **k): pass + c.actions = [ + (1, f, (1,)), + ] + c.execute_actions(clear=False) + self.assertEqual(c.actions, [ + (1, f, (1,)), + (3, g, (8,)), + ]) + class Test_resolveConflicts(unittest.TestCase): def _callFUT(self, actions): from pyramid.config import resolveConflicts -- cgit v1.2.3 From 4a7029f6b313b65ba94d0726042ea3adbad38e81 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 26 Dec 2014 22:48:41 -0800 Subject: Raise errors if unbound methods are passed in --- pyramid/compat.py | 17 ++++++++++++ pyramid/config/views.py | 15 +++++------ pyramid/tests/test_compat.py | 46 +++++++++++++++++++++++++++++++++ pyramid/tests/test_config/test_views.py | 21 +++++---------- 4 files changed, 76 insertions(+), 23 deletions(-) create mode 100644 pyramid/tests/test_compat.py diff --git a/pyramid/compat.py b/pyramid/compat.py index bfa345b88..749435ebc 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -244,3 +244,20 @@ else: def is_bound_method(ob): return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None +def is_unbound_method(fn): + """ + This consistently verifies that the callable is bound to a + class. + """ + is_bound = is_bound_method(fn) + + if not is_bound and inspect.isroutine(fn): + spec = inspect.getargspec(fn) + has_self = len(spec.args) > 0 and spec.args[0] == 'self' + + if PY3 and inspect.isfunction(fn) and has_self: # pragma: no cover + return True + elif inspect.ismethod(fn): + return True + + return False diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 3e305055f..d498395e1 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -42,6 +42,7 @@ from pyramid.compat import ( url_quote, WIN, is_bound_method, + is_unbound_method, is_nonstr_iter, im_self, ) @@ -419,15 +420,11 @@ class DefaultViewMapper(object): self.attr = kw.get('attr') def __call__(self, view): - # Map the attr directly if the passed in view is method and a - # constructor is defined and must be unbound (for backwards - # compatibility) - if inspect.ismethod(view): - is_bound = getattr(view, im_self, None) is not None - - if not is_bound: - self.attr = view.__name__ - view = view.im_class + if is_unbound_method(view) and self.attr is None: + raise ConfigurationError(( + 'Unbound method calls are not supported, please set the class ' + 'as your `view` and the method as your `attr`' + )) if inspect.isclass(view): view = self.map_class(view) diff --git a/pyramid/tests/test_compat.py b/pyramid/tests/test_compat.py new file mode 100644 index 000000000..2f80100dd --- /dev/null +++ b/pyramid/tests/test_compat.py @@ -0,0 +1,46 @@ +import unittest + +class TestUnboundMethods(unittest.TestCase): + def test_old_style_bound(self): + from pyramid.compat import is_unbound_method + + class OldStyle: + def run(self): + return 'OK' + + self.assertFalse(is_unbound_method(OldStyle().run)) + + def test_new_style_bound(self): + from pyramid.compat import is_unbound_method + + class NewStyle(object): + def run(self): + return 'OK' + + self.assertFalse(is_unbound_method(NewStyle().run)) + + def test_old_style_unbound(self): + from pyramid.compat import is_unbound_method + + class OldStyle: + def run(self): + return 'OK' + + self.assertTrue(is_unbound_method(OldStyle.run)) + + def test_new_style_unbound(self): + from pyramid.compat import is_unbound_method + + class NewStyle(object): + def run(self): + return 'OK' + + self.assertTrue(is_unbound_method(NewStyle.run)) + + def test_normal_func_unbound(self): + from pyramid.compat import is_unbound_method + + def func(): + return 'OK' + + self.assertFalse(is_unbound_method(func)) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 664208fad..d1eb1ed3c 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1669,23 +1669,16 @@ class TestViewsConfigurationMixin(unittest.TestCase): def test_add_view_class_method_no_attr(self): from pyramid.renderers import null_renderer from zope.interface import directlyProvides - - class ViewClass(object): - def __init__(self, request): - self.request = request - - def run(self): - return 'OK' + from pyramid.exceptions import ConfigurationError config = self._makeOne(autocommit=True) - config.add_view(view=ViewClass.run, renderer=null_renderer) + class DummyViewClass(object): + def run(self): pass - wrapper = self._getViewCallable(config) - context = DummyContext() - directlyProvides(context, IDummy) - request = self._makeRequest(config) - result = wrapper(context, request) - self.assertEqual(result, 'OK') + def configure_view(): + config.add_view(view=DummyViewClass.run, renderer=null_renderer) + + self.assertRaises(ConfigurationError, configure_view) def test_derive_view_function(self): from pyramid.renderers import null_renderer -- cgit v1.2.3 From 6d4676137885f63f364a2b2ae6205c6931a57220 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 26 Dec 2014 23:04:56 -0800 Subject: Don't need im_self --- pyramid/config/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index d498395e1..afacc1e0b 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -44,7 +44,6 @@ from pyramid.compat import ( is_bound_method, is_unbound_method, is_nonstr_iter, - im_self, ) from pyramid.exceptions import ( -- cgit v1.2.3 From 03a0d79306b2846313df1983a721d5cccf4ec3ce Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 26 Dec 2014 23:19:32 -0800 Subject: Clean up compat tests --- pyramid/tests/test_compat.py | 36 ++++++++---------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/pyramid/tests/test_compat.py b/pyramid/tests/test_compat.py index 2f80100dd..23ccce82e 100644 --- a/pyramid/tests/test_compat.py +++ b/pyramid/tests/test_compat.py @@ -1,46 +1,26 @@ import unittest +from pyramid.compat import is_unbound_method class TestUnboundMethods(unittest.TestCase): def test_old_style_bound(self): - from pyramid.compat import is_unbound_method - - class OldStyle: - def run(self): - return 'OK' - self.assertFalse(is_unbound_method(OldStyle().run)) def test_new_style_bound(self): - from pyramid.compat import is_unbound_method - - class NewStyle(object): - def run(self): - return 'OK' - self.assertFalse(is_unbound_method(NewStyle().run)) def test_old_style_unbound(self): - from pyramid.compat import is_unbound_method - - class OldStyle: - def run(self): - return 'OK' - self.assertTrue(is_unbound_method(OldStyle.run)) def test_new_style_unbound(self): - from pyramid.compat import is_unbound_method - - class NewStyle(object): - def run(self): - return 'OK' - self.assertTrue(is_unbound_method(NewStyle.run)) def test_normal_func_unbound(self): - from pyramid.compat import is_unbound_method - - def func(): - return 'OK' + def func(): return 'OK' self.assertFalse(is_unbound_method(func)) + +class OldStyle: + def run(self): return 'OK' + +class NewStyle(object): + def run(self): return 'OK' -- cgit v1.2.3 From c569571bdb6e8c001ab0bc11777a2e0cca72d2fb Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 27 Dec 2014 01:55:25 -0600 Subject: add action-order documentation --- docs/narr/extconfig.rst | 99 +++++++++++++++++++++++++++++++++++++++++++++- pyramid/config/__init__.py | 2 +- 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index 6587aef92..c4d3e0250 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -215,13 +215,110 @@ registers an action with a higher order than the passed to it, that a route by this name was already registered by ``add_route``, and if such a route has not already been registered, it's a configuration error (a view that names a nonexistent route via its -``route_name`` parameter will never be called). +``route_name`` parameter will never be called). As of Pyramid 1.6 it is +possible for one action to invoke another. See :ref:`ordering_actions` for +more information. ``introspectables`` is a sequence of :term:`introspectable` objects. You can pass a sequence of introspectables to the :meth:`~pyramid.config.Configurator.action` method, which allows you to augment Pyramid's configuration introspection system. +.. _ordering_actions: + +Ordering Actions +---------------- + +In Pyramid every :term:`action` has an inherent ordering relative to other +actions. The logic within actions is deferred until a call to +:meth:`pyramid.config.Configurator.commit` (which is automatically invoked by +:meth:`pyramid.config.Configurator.make_wsgi_app`). This means you may call +``config.add_view(route_name='foo')`` **before** +``config.add_route('foo', '/foo')`` because nothing actually happens until +commit-time when conflicts are resolved, actions are ordered and executed. + +By default, almost every action in Pyramid has an ``order`` of ``0``. Every +action within the same order-level will be executed in the order it was called. +This means that if an action must be reliably executed before or after another +action, the ``order`` must be defined explicitly to make this work. For +example, views are dependent on routes being defined. Thus the action created +by :meth:`pyramid.config.Configurator.add_route` has an ``order`` of +:const:`pyramid.interfaces.PHASE2_CONFIG`. + +Pre-defined Phases +~~~~~~~~~~~~~~~~~~ + +:const:`pyramid.interfaces.PHASE1_CONFIG` + +- :meth:`pyramid.config.Configurator.add_renderer` +- :meth:`pyramid.config.Configurator.add_route_predicate` +- :meth:`pyramid.config.Configurator.add_subscriber_predicate` +- :meth:`pyramid.config.Configurator.add_view_predicate` +- :meth:`pyramid.config.Configurator.set_authorization_policy` +- :meth:`pyramid.config.Configurator.set_default_permission` +- :meth:`pyramid.config.Configurator.set_view_mapper` + +:const:`pyramid.interfaces.PHASE2_CONFIG` + +- :meth:`pyramid.config.Configurator.add_route` +- :meth:`pyramid.config.Configurator.set_authentication_policy` + +``0`` + +- The default for all builtin or custom directives unless otherwise specified. + +Calling Actions From Actions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.6 + +Pyramid's configurator allows actions to be added during a commit-cycle as +long as they are added to the current or a later ``order`` phase. This means +that your custom action can defer decisions until commit-time and then do +things like invoke :meth:`pyramid.config.Configurator.add_route`. It can also +provide better conflict detection if your addon needs to call more than one +other action. + +For example, let's make an addon that invokes ``add_route`` and ``add_view``, +but we want it to conflict with any other call to our addon: + +.. code-block:: python + :linenos: + + from pyramid.interfaces import PHASE1_CONFIG + + PHASE0_CONFIG = PHASE1_CONFIG - 10 + + def includeme(config): + config.add_directive(add_auto_route, 'add_auto_route') + + def add_auto_route(config, name, view): + def register(): + config.add_view(route_name=name, view=view) + config.add_route(name, '/' + name) + config.action(('auto route', name), register, order=PHASE0_CONFIG) + +Now someone else can use your addon and be informed if there is a conflict +between this route and another, or two calls to ``add_auto_route``. +Notice how we had to invoke our action **before** ``add_view`` or +``add_route``. If we tried to invoke this afterward, the subsequent calls to +``add_view`` and ``add_route`` would cause conflicts because that phase had +already been executed, and the configurator cannot go back in time to add more +views during that commit-cycle. + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + def main(global_config, **settings): + config = Configurator() + config.include('auto_route_addon') + config.add_auto_route('foo', my_view) + + def my_view(request): + return request.response + .. _introspection: Adding Configuration Introspection diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index e81ccee3f..a114cf039 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1075,7 +1075,7 @@ class ActionState(object): ... ] >>> context.execute_actions() >>> output - [('f', (1,), {}), ('f', (2,), {}), ('g', (8,), {})] + [('f', (1,), {}), ('g', (8,), {})] """ try: -- cgit v1.2.3 From d35a916095943b020f30acb90e878abe9bfd4fb1 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 27 Dec 2014 01:58:59 -0600 Subject: update changelog --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 46c331268..b60600198 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,12 @@ Next release Features -------- +- The ``pyramid.config.Configurator`` has grown the ability to allow + actions to call other actions during a commit-cycle. This enables much more + logic to be placed into actions, such as the ability to invoke other actions + or group them for improved conflict detection. + See https://github.com/Pylons/pyramid/pull/1513 + - Added support / testing for 'pypy3' under Tox and Travis. See https://github.com/Pylons/pyramid/pull/1469 -- cgit v1.2.3 From 1236dec0dcfd916bca4e233587f86baa8d2418a8 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sat, 27 Dec 2014 00:18:23 -0800 Subject: Add the `set_response_factory` API --- docs/narr/hooks.rst | 62 +++++++++++++++++++++++++++++ pyramid/config/__init__.py | 16 +++++++- pyramid/config/factories.py | 27 +++++++++++++ pyramid/tests/test_config/test_factories.py | 15 +++++++ pyramid/tests/test_config/test_init.py | 12 ++++++ 5 files changed, 130 insertions(+), 2 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 4da36e730..f557527bb 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -354,6 +354,68 @@ We attach and cache an object named ``extra`` to the ``request`` object. .. _beforerender_event: +.. index:: + single: response factory + +.. _changing_the_response_factory: + +Changing the Response Factory +---------------------------- + +Whenever :app:`Pyramid` returns a response from a view it creates a +:term:`response` object. By default, an instance of the +:class:`pyramid.response.Response` class is created to represent the response +object. + +The class (aka "factory") that :app:`Pyramid` uses to create a response object +instance can be changed by passing a ``response_factory`` argument to the +constructor of the :term:`configurator`. This argument can be either a +callable or a :term:`dotted Python name` representing a callable. + +.. code-block:: python + :linenos: + + from pyramid.response import Response + + class MyResponse(Response): + pass + + config = Configurator(response_factory=MyResponse) + +If you're doing imperative configuration, and you'd rather do it after you've +already constructed a :term:`configurator` it can also be registered via the +:meth:`pyramid.config.Configurator.set_response_factory` method: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + from pyramid.response import Response + + class MyResponse(Response): + pass + + config = Configurator() + config.set_response_factory(MyRequest) + +If you are already using a custom ```request_factory`` you can also set the +``ResponseClass`` on your :class:`pyramid.request.Request`: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + from pyramid.response import Response + from pyramid.request import Request + + class MyResponse(Response): + pass + + class MyRequest(Request): + ResponseClass = MyResponse + + config = Configurator() + Using The Before Render Event ----------------------------- diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index cfa35ec6c..2ab654b9a 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -179,6 +179,11 @@ class Configurator( See :ref:`changing_the_request_factory`. By default it is ``None``, which means use the default request factory. + If ``response_factory`` is passed, it should be a :term:`response + factory` implementation or a :term:`dotted Python name` to the same. + See :ref:`changing_the_response_factory`. By default it is ``None``, + which means use the default response factory. + If ``default_permission`` is passed, it should be a :term:`permission` string to be used as the default permission for all view configuration registrations performed against this @@ -190,7 +195,7 @@ class Configurator( configurations which do not explicitly declare a permission will always be executable by entirely anonymous users (any authorization policy in effect is ignored). - + .. seealso:: See also :ref:`setting_a_default_permission`. @@ -254,6 +259,7 @@ class Configurator( .. versionadded:: 1.6 The ``root_package`` argument. + The ``response_factory`` argument. """ manager = manager # for testing injection venusian = venusian # for testing injection @@ -276,6 +282,7 @@ class Configurator( debug_logger=None, locale_negotiator=None, request_factory=None, + response_factory=None, default_permission=None, session_factory=None, default_view_mapper=None, @@ -310,6 +317,7 @@ class Configurator( debug_logger=debug_logger, locale_negotiator=locale_negotiator, request_factory=request_factory, + response_factory=response_factory, default_permission=default_permission, session_factory=session_factory, default_view_mapper=default_view_mapper, @@ -325,6 +333,7 @@ class Configurator( debug_logger=None, locale_negotiator=None, request_factory=None, + response_factory=None, default_permission=None, session_factory=None, default_view_mapper=None, @@ -412,6 +421,9 @@ class Configurator( if request_factory: self.set_request_factory(request_factory) + if response_factory: + self.set_response_factory(response_factory) + if default_permission: self.set_default_permission(default_permission) @@ -469,7 +481,7 @@ class Configurator( _registry.registerSelfAdapter = registerSelfAdapter # API - + def _get_introspector(self): introspector = getattr(self.registry, 'introspector', _marker) if introspector is _marker: diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index 5ce1081c6..d7a48ba93 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -4,6 +4,7 @@ from zope.interface import implementer from pyramid.interfaces import ( IDefaultRootFactory, IRequestFactory, + IResponseFactory, IRequestExtensions, IRootFactory, ISessionFactory, @@ -96,6 +97,32 @@ class FactoriesConfiguratorMixin(object): intr['factory'] = factory self.action(IRequestFactory, register, introspectables=(intr,)) + @action_method + def set_response_factory(self, factory): + """ The object passed as ``factory`` should be an object (or a + :term:`dotted Python name` which refers to an object) which + will be used by the :app:`Pyramid` as the default response + objects. This factory object must have the same + methods and attributes as the + :class:`pyramid.request.Response` class. + + .. note:: + + Using the ``response_factory`` argument to the + :class:`pyramid.config.Configurator` constructor + can be used to achieve the same purpose. + """ + factory = self.maybe_dotted(factory) + + def register(): + self.registry.registerUtility(factory, IResponseFactory) + + intr = self.introspectable('response factory', None, + self.object_description(factory), + 'response factory') + intr['factory'] = factory + self.action(IResponseFactory, register, introspectables=(intr,)) + @action_method def add_request_method(self, callable=None, diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index 6e679397f..c6785d4a5 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -23,6 +23,21 @@ class TestFactoriesMixin(unittest.TestCase): self.assertEqual(config.registry.getUtility(IRequestFactory), dummyfactory) + def test_set_response_factory(self): + from pyramid.interfaces import IResponseFactory + config = self._makeOne(autocommit=True) + factory = object() + config.set_response_factory(factory) + self.assertEqual(config.registry.getUtility(IResponseFactory), factory) + + def test_set_response_factory_dottedname(self): + from pyramid.interfaces import IResponseFactory + config = self._makeOne(autocommit=True) + config.set_response_factory( + 'pyramid.tests.test_config.dummyfactory') + self.assertEqual(config.registry.getUtility(IResponseFactory), + dummyfactory) + def test_set_root_factory(self): from pyramid.interfaces import IRootFactory config = self._makeOne() diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index c9eaf7c27..8fa6be011 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -546,6 +546,18 @@ class ConfiguratorTests(unittest.TestCase): utility = reg.getUtility(IRequestFactory) self.assertEqual(utility, factory) + def test_setup_registry_response_factory(self): + from pyramid.registry import Registry + from pyramid.interfaces import IResponseFactory + reg = Registry() + config = self._makeOne(reg) + factory = object() + config.setup_registry(response_factory=factory) + self.assertEqual(reg.queryUtility(IResponseFactory), None) + config.commit() + utility = reg.getUtility(IResponseFactory) + self.assertEqual(utility, factory) + def test_setup_registry_request_factory_custom_response_class(self): from pyramid.registry import Registry from pyramid.interfaces import IRequestFactory -- cgit v1.2.3 From e8a666655b5365a0adde32f2bd387b0d42690384 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sat, 27 Dec 2014 00:23:18 -0800 Subject: basic docs cleanup --- docs/glossary.rst | 3 +++ docs/narr/hooks.rst | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 01300a0be..05ff7c114 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -16,6 +16,9 @@ Glossary An object which, provided a :term:`WSGI` environment as a single positional argument, returns a Pyramid-compatible request. + response factory + An object which returns a Pyramid-compatible response. + response An object returned by a :term:`view callable` that represents response data returned to the requesting user agent. It must implement the diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index f557527bb..4702c09b0 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -360,7 +360,7 @@ We attach and cache an object named ``extra`` to the ``request`` object. .. _changing_the_response_factory: Changing the Response Factory ----------------------------- +------------------------------- Whenever :app:`Pyramid` returns a response from a view it creates a :term:`response` object. By default, an instance of the -- cgit v1.2.3 From 807e941787e157db882fcd95e13f5cafb7ebde7f Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sat, 27 Dec 2014 13:33:39 -0800 Subject: Added a version added flag --- docs/narr/hooks.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 4702c09b0..689ce9dc2 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -362,6 +362,8 @@ We attach and cache an object named ``extra`` to the ``request`` object. Changing the Response Factory ------------------------------- +.. versionadded:: 1.6 + Whenever :app:`Pyramid` returns a response from a view it creates a :term:`response` object. By default, an instance of the :class:`pyramid.response.Response` class is created to represent the response -- cgit v1.2.3 From 32cb805132e8149a276a8c65fdfa961384e8254e Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 1 Jan 2015 15:03:56 -0800 Subject: Mkae the response factory a factory that takes a request --- docs/glossary.rst | 3 ++- docs/narr/hooks.rst | 29 ++++++-------------------- pyramid/renderers.py | 4 ++-- pyramid/request.py | 4 ++-- pyramid/testing.py | 10 +++++---- pyramid/tests/test_config/test_factories.py | 2 +- pyramid/tests/test_config/test_init.py | 32 +---------------------------- pyramid/tests/test_renderers.py | 9 ++++++-- pyramid/tests/test_testing.py | 4 +++- pyramid/tests/test_util.py | 26 +++++------------------ pyramid/util.py | 13 ++++-------- 11 files changed, 39 insertions(+), 97 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 05ff7c114..38133f68f 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -17,7 +17,8 @@ Glossary positional argument, returns a Pyramid-compatible request. response factory - An object which returns a Pyramid-compatible response. + An object which, provied a :term:`request` as a single positional + argument, returns a Pyramid-compatible response. response An object returned by a :term:`view callable` that represents response diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 689ce9dc2..e250c2d7e 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -369,10 +369,10 @@ Whenever :app:`Pyramid` returns a response from a view it creates a :class:`pyramid.response.Response` class is created to represent the response object. -The class (aka "factory") that :app:`Pyramid` uses to create a response object -instance can be changed by passing a ``response_factory`` argument to the -constructor of the :term:`configurator`. This argument can be either a -callable or a :term:`dotted Python name` representing a callable. +The factory that :app:`Pyramid` uses to create a response object instance can be +changed by passing a ``response_factory`` argument to the constructor of the +:term:`configurator`. This argument can be either a callable or a +:term:`dotted Python name` representing a callable. .. code-block:: python :linenos: @@ -382,7 +382,7 @@ callable or a :term:`dotted Python name` representing a callable. class MyResponse(Response): pass - config = Configurator(response_factory=MyResponse) + config = Configurator(response_factory=lambda r: MyResponse()) If you're doing imperative configuration, and you'd rather do it after you've already constructed a :term:`configurator` it can also be registered via the @@ -398,25 +398,8 @@ already constructed a :term:`configurator` it can also be registered via the pass config = Configurator() - config.set_response_factory(MyRequest) - -If you are already using a custom ```request_factory`` you can also set the -``ResponseClass`` on your :class:`pyramid.request.Request`: - -.. code-block:: python - :linenos: - - from pyramid.config import Configurator - from pyramid.response import Response - from pyramid.request import Request - - class MyResponse(Response): - pass - - class MyRequest(Request): - ResponseClass = MyResponse + config.set_response_factory(lambda r: MyResponse()) - config = Configurator() Using The Before Render Event ----------------------------- diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 3d5390840..51dbd318b 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -449,8 +449,8 @@ class RendererHelper(object): if response is None: # request is None or request is not a pyramid.response.Response registry = self.registry - response_factory = _get_response_factory(registry, request) - response = response_factory() + response_factory = _get_response_factory(registry) + response = response_factory(request) if result is not None: if isinstance(result, text_type): diff --git a/pyramid/request.py b/pyramid/request.py index b66b8926c..fc957fae2 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -216,8 +216,8 @@ class Request( right" attributes (e.g. by calling ``request.response.set_cookie()``) within a view that uses a renderer. Mutations to this response object will be preserved in the response sent to the client.""" - response_factory = _get_response_factory(self.registry, self) - return response_factory() + response_factory = _get_response_factory(self.registry) + return response_factory(self) def is_response(self, ob): """ Return ``True`` if the object passed as ``ob`` is a valid diff --git a/pyramid/testing.py b/pyramid/testing.py index f77889e72..66c694b31 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -9,7 +9,6 @@ from zope.interface import ( from pyramid.interfaces import ( IRequest, - IResponseFactory, ISession, ) @@ -40,7 +39,10 @@ from pyramid.threadlocal import ( from pyramid.i18n import LocalizerRequestMixin from pyramid.request import CallbackMethodsMixin from pyramid.url import URLMethodsMixin -from pyramid.util import InstancePropertyMixin +from pyramid.util import ( + InstancePropertyMixin, + _get_response_factory + ) _marker = object() @@ -383,8 +385,8 @@ class DummyRequest( @reify def response(self): - f = self.registry.queryUtility(IResponseFactory, default=Response) - return f() + f = _get_response_factory(self.registry) + return f(self) have_zca = True diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index c6785d4a5..0bd5336ff 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -26,7 +26,7 @@ class TestFactoriesMixin(unittest.TestCase): def test_set_response_factory(self): from pyramid.interfaces import IResponseFactory config = self._makeOne(autocommit=True) - factory = object() + factory = lambda r: object() config.set_response_factory(factory) self.assertEqual(config.registry.getUtility(IResponseFactory), factory) diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 8fa6be011..aeebe3c91 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -551,43 +551,13 @@ class ConfiguratorTests(unittest.TestCase): from pyramid.interfaces import IResponseFactory reg = Registry() config = self._makeOne(reg) - factory = object() + factory = lambda r: object() config.setup_registry(response_factory=factory) self.assertEqual(reg.queryUtility(IResponseFactory), None) config.commit() utility = reg.getUtility(IResponseFactory) self.assertEqual(utility, factory) - def test_setup_registry_request_factory_custom_response_class(self): - from pyramid.registry import Registry - from pyramid.interfaces import IRequestFactory - from pyramid.request import Request - - class MyResponse(object): - pass - - class MyRequest(Request): - ResponseClass = MyResponse - - reg = Registry() - config = self._makeOne(reg) - factory = MyRequest({ - 'PATH_INFO': '/', - 'wsgi.url_scheme': 'http', - 'HTTP_HOST': 'localhost', - 'SERVER_PROTOCOL': '1.0', - }) - factory.registry = reg - - config.setup_registry(request_factory=factory) - self.assertEqual(reg.queryUtility(IRequestFactory), None) - config.commit() - utility = reg.getUtility(IRequestFactory) - self.assertEqual(utility, factory) - - new_response = factory.response - self.assertTrue(isinstance(new_response, MyResponse)) - def test_setup_registry_request_factory_dottedname(self): from pyramid.registry import Registry from pyramid.interfaces import IRequestFactory diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 2bddd2318..21878b41f 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -182,7 +182,10 @@ class TestRendererHelper(unittest.TestCase): from pyramid.interfaces import IResponseFactory class ResponseFactory(object): pass - self.config.registry.registerUtility(ResponseFactory, IResponseFactory) + + self.config.registry.registerUtility( + lambda r: ResponseFactory(), IResponseFactory + ) def test_render_to_response(self): self._registerRendererFactory() @@ -310,7 +313,9 @@ class TestRendererHelper(unittest.TestCase): class ResponseFactory(object): def __init__(self): pass - self.config.registry.registerUtility(ResponseFactory, IResponseFactory) + self.config.registry.registerUtility( + lambda r: ResponseFactory(), IResponseFactory + ) request = testing.DummyRequest() helper = self._makeOne('loo.foo') response = helper._make_response(b'abc', request) diff --git a/pyramid/tests/test_testing.py b/pyramid/tests/test_testing.py index dfcad2a0c..113f7e5f4 100644 --- a/pyramid/tests/test_testing.py +++ b/pyramid/tests/test_testing.py @@ -259,7 +259,9 @@ class TestDummyRequest(unittest.TestCase): registry = Registry('this_test') class ResponseFactory(object): pass - registry.registerUtility(ResponseFactory, IResponseFactory) + registry.registerUtility( + lambda r: ResponseFactory(), IResponseFactory + ) request = self._makeOne() request.registry = registry resp = request.response diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 0ebbd80a2..ba128eede 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -324,7 +324,7 @@ class Test_object_description(unittest.TestCase): self.assertEqual( self._callFUT(inst), "object %s" % str(inst)) - + def test_shortened_repr(self): inst = ['1'] * 1000 self.assertEqual( @@ -592,7 +592,7 @@ class TestActionInfo(unittest.TestCase): def _getTargetClass(self): from pyramid.util import ActionInfo return ActionInfo - + def _makeOne(self, filename, lineno, function, linerepr): return self._getTargetClass()(filename, lineno, function, linerepr) @@ -620,30 +620,14 @@ class TestActionInfo(unittest.TestCase): class TestGetResponseFactory(unittest.TestCase): - def test_no_request(self): + def test_get_factory(self): from pyramid.util import _get_response_factory from pyramid.registry import Registry from pyramid.response import Response registry = Registry() - factory = _get_response_factory(registry) - self.assertEqual(factory, Response) - - def test_with_request(self): - from pyramid.util import _get_response_factory - from pyramid.registry import Registry - from pyramid.request import Request - - class MyResponse(object): - pass - - class MyRequest(Request): - ResponseClass = MyResponse - registry = Registry() - - request = MyRequest({}) - factory = _get_response_factory(request.registry, request) - self.assertEqual(factory, MyResponse) + response = _get_response_factory(registry)(None) + self.assertTrue(isinstance(response, Response)) def dummyfunc(): pass diff --git a/pyramid/util.py b/pyramid/util.py index ee642164a..7aecca19c 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -556,19 +556,14 @@ def action_method(wrapped): wrapper.__docobj__ = wrapped return wrapper -def _get_response_factory(registry, request=None): + +def _get_response_factory(registry): """ Obtain a :class: `pyramid.response.Response` using the - ``request.ResponseClass`` property if available. + `pyramid.interfaces.IResponseFactory`. """ - # Request is `None` or does not have a `ResponseClass` - if hasattr(request, 'ResponseClass'): - response_class = request.ResponseClass - else: - response_class = Response - response_factory = registry.queryUtility( IResponseFactory, - default=response_class + default=lambda r: Response() ) return response_factory -- cgit v1.2.3 From ff01cdf0e392eb4e7926970a0cdee75663edb431 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 1 Jan 2015 15:10:55 -0800 Subject: Fix typo --- docs/glossary.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 38133f68f..911c22075 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -17,7 +17,7 @@ Glossary positional argument, returns a Pyramid-compatible request. response factory - An object which, provied a :term:`request` as a single positional + An object which, provided a :term:`request` as a single positional argument, returns a Pyramid-compatible response. response -- cgit v1.2.3 From a62462606704081b37ae0cb4c9f07a4690480609 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 1 Jan 2015 15:26:00 -0800 Subject: Added CHANGES entry for this PR --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 46c331268..129ce4616 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -59,6 +59,11 @@ Features via ``request.static_url('myapp:static/foo.png')``. See https://github.com/Pylons/pyramid/issues/1252 +- Added ``pyramid.config.Configurator.set_response_factory`` and the + ``response_factory`` keyword argument to the ``Configurator`` for defining + a factory that will return a custom ``Response`` class. + See https://github.com/Pylons/pyramid/pull/1499 + Bug Fixes --------- -- cgit v1.2.3 From 0882f7f0f4648b37617543a196ec4d5bf6d65278 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 1 Jan 2015 17:42:57 -0800 Subject: Setup logging with prequest --- pyramid/scripts/prequest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyramid/scripts/prequest.py b/pyramid/scripts/prequest.py index 2ab3b8bb9..92faa146c 100644 --- a/pyramid/scripts/prequest.py +++ b/pyramid/scripts/prequest.py @@ -5,7 +5,7 @@ import textwrap from pyramid.compat import url_unquote from pyramid.request import Request -from pyramid.paster import get_app +from pyramid.paster import get_app, setup_logging from pyramid.scripts.common import parse_vars def main(argv=sys.argv, quiet=False): @@ -102,6 +102,7 @@ class PRequestCommand(object): self.out('You must provide at least two arguments') return 2 app_spec = self.args[0] + setup_logging(app_spec) path = self.args[1] if not path.startswith('/'): path = '/' + path -- cgit v1.2.3 From aa9aef58b5b2fe4c9135aadc5481d129f1300154 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 1 Jan 2015 17:56:13 -0800 Subject: Added a test for configuring logging --- pyramid/scripts/prequest.py | 7 ++++++- pyramid/tests/test_scripts/test_prequest.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pyramid/scripts/prequest.py b/pyramid/scripts/prequest.py index 92faa146c..34eeadf32 100644 --- a/pyramid/scripts/prequest.py +++ b/pyramid/scripts/prequest.py @@ -97,13 +97,18 @@ class PRequestCommand(object): if not self.quiet: print(msg) + def configure_logging(self, app_spec): + setup_logging(app_spec) + def run(self): if not len(self.args) >= 2: self.out('You must provide at least two arguments') return 2 app_spec = self.args[0] - setup_logging(app_spec) path = self.args[1] + + self.configure_logging(app_spec) + if not path.startswith('/'): path = '/' + path diff --git a/pyramid/tests/test_scripts/test_prequest.py b/pyramid/tests/test_scripts/test_prequest.py index 37f1d3c0f..95cec0518 100644 --- a/pyramid/tests/test_scripts/test_prequest.py +++ b/pyramid/tests/test_scripts/test_prequest.py @@ -210,8 +210,21 @@ class TestPRequestCommand(unittest.TestCase): self.assertEqual(self._path_info, '/') self.assertEqual(self._spec, 'development.ini') self.assertEqual(self._app_name, None) + self.assertEqual(self._out, [b'abc']) + def test_command_method_configures_logging(self): + command = self._makeOne(['', 'development.ini', '/']) + called_args = [] + + def configure_logging(app_spec): + called_args.append(app_spec) + + command.configure_logging = configure_logging + command.run() + self.assertEqual(called_args, ['development.ini']) + + class Test_main(unittest.TestCase): def _callFUT(self, argv): from pyramid.scripts.prequest import main -- cgit v1.2.3 From 9e7248258800ef2c7072f497901172ab94988708 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 1 Jan 2015 20:06:48 -0800 Subject: Catch bad `name` and raise a `ValueError` --- pyramid/tests/test_config/test_factories.py | 18 +++++++++++++++++- pyramid/util.py | 10 +++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index 6e679397f..5ae486c4b 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -111,6 +111,22 @@ class TestFactoriesMixin(unittest.TestCase): config = self._makeOne(autocommit=True) self.assertRaises(AttributeError, config.add_request_method) + def test_add_request_method_with_text_type_name(self): + from pyramid.interfaces import IRequestExtensions + from pyramid.compat import text_ + from pyramid.util import InstancePropertyMixin + + config = self._makeOne(autocommit=True) + def boomshaka(r): pass + name = text_(b'La Pe\xc3\xb1a', 'utf-8') + config.add_request_method(boomshaka, name=name) + exts = config.registry.getUtility(IRequestExtensions) + inst = InstancePropertyMixin() + + def set_extensions(): + inst._set_extensions(exts) + self.assertRaises(ValueError, set_extensions) + self.assertTrue(name in exts.methods) class TestDeprecatedFactoriesMixinMethods(unittest.TestCase): def setUp(self): @@ -120,7 +136,7 @@ class TestDeprecatedFactoriesMixinMethods(unittest.TestCase): def tearDown(self): from zope.deprecation import __show__ __show__.on() - + def _makeOne(self, *arg, **kw): from pyramid.config import Configurator config = Configurator(*arg, **kw) diff --git a/pyramid/util.py b/pyramid/util.py index 6de53d559..e9f5760a6 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -111,7 +111,15 @@ class InstancePropertyMixin(object): def _set_extensions(self, extensions): for name, fn in iteritems_(extensions.methods): method = fn.__get__(self, self.__class__) - setattr(self, name, method) + try: + setattr(self, name, method) + except UnicodeEncodeError: + msg = ( + '`name="%s"` is invalid. `name` must be ascii because it is ' + 'used on __name__ of the method' + ) + raise ValueError(msg % name) + self._set_properties(extensions.descriptors) def set_property(self, callable, name=None, reify=False): -- cgit v1.2.3 From 303abc8b585d97c75773b3cfa48b6e748c96fd64 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 1 Jan 2015 20:15:12 -0800 Subject: Move _get_response_factory to pyramid.response --- pyramid/renderers.py | 4 +--- pyramid/request.py | 7 ++----- pyramid/response.py | 17 +++++++++++++++-- pyramid/testing.py | 8 +++----- pyramid/tests/test_response.py | 17 +++++++++++++---- pyramid/tests/test_util.py | 11 ----------- pyramid/util.py | 12 ------------ 7 files changed, 34 insertions(+), 42 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 51dbd318b..d57671865 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -18,15 +18,13 @@ from pyramid.compat import ( text_type, ) -from pyramid.util import _get_response_factory - from pyramid.decorator import reify from pyramid.events import BeforeRender from pyramid.path import caller_package -from pyramid.response import Response +from pyramid.response import Response, _get_response_factory from pyramid.threadlocal import get_current_registry # API diff --git a/pyramid/request.py b/pyramid/request.py index fc957fae2..b2e2efe05 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -20,16 +20,13 @@ from pyramid.compat import ( from pyramid.decorator import reify from pyramid.i18n import LocalizerRequestMixin -from pyramid.response import Response +from pyramid.response import Response, _get_response_factory from pyramid.security import ( AuthenticationAPIMixin, AuthorizationAPIMixin, ) from pyramid.url import URLMethodsMixin -from pyramid.util import ( - InstancePropertyMixin, - _get_response_factory, -) +from pyramid.util import InstancePropertyMixin class TemplateContext(object): pass diff --git a/pyramid/response.py b/pyramid/response.py index d11fd0123..892e5dfff 100644 --- a/pyramid/response.py +++ b/pyramid/response.py @@ -8,7 +8,8 @@ import venusian from webob import Response as _Response from zope.interface import implementer -from pyramid.interfaces import IResponse +from pyramid.interfaces import IResponse, IResponseFactory + def init_mimetypes(mimetypes): # this is a function so it can be unittested @@ -143,7 +144,7 @@ class response_adapter(object): @response_adapter(dict, list) def myadapter(ob): return Response(json.dumps(ob)) - + This method will have no effect until a :term:`scan` is performed agains the package or module which contains it, ala: @@ -167,3 +168,15 @@ class response_adapter(object): def __call__(self, wrapped): self.venusian.attach(wrapped, self.register, category='pyramid') return wrapped + + +def _get_response_factory(registry): + """ Obtain a :class: `pyramid.response.Response` using the + `pyramid.interfaces.IResponseFactory`. + """ + response_factory = registry.queryUtility( + IResponseFactory, + default=lambda r: Response() + ) + + return response_factory diff --git a/pyramid/testing.py b/pyramid/testing.py index 66c694b31..667e6af4e 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -21,7 +21,7 @@ from pyramid.compat import ( from pyramid.config import Configurator from pyramid.decorator import reify from pyramid.path import caller_package -from pyramid.response import Response +from pyramid.response import Response, _get_response_factory from pyramid.registry import Registry from pyramid.security import ( @@ -39,10 +39,8 @@ from pyramid.threadlocal import ( from pyramid.i18n import LocalizerRequestMixin from pyramid.request import CallbackMethodsMixin from pyramid.url import URLMethodsMixin -from pyramid.util import ( - InstancePropertyMixin, - _get_response_factory - ) +from pyramid.util import InstancePropertyMixin + _marker = object() diff --git a/pyramid/tests/test_response.py b/pyramid/tests/test_response.py index 84ec57757..ad55882c9 100644 --- a/pyramid/tests/test_response.py +++ b/pyramid/tests/test_response.py @@ -8,7 +8,7 @@ class TestResponse(unittest.TestCase): def _getTargetClass(self): from pyramid.response import Response return Response - + def test_implements_IResponse(self): from pyramid.interfaces import IResponse cls = self._getTargetClass() @@ -119,7 +119,7 @@ class Test_patch_mimetypes(unittest.TestCase): result = self._callFUT(module) self.assertEqual(result, True) self.assertEqual(module.initted, True) - + def test_missing_init(self): class DummyMimetypes(object): pass @@ -174,6 +174,17 @@ class TestResponseAdapter(unittest.TestCase): self.assertEqual(dummy_venusian.attached, [(foo, dec.register, 'pyramid')]) + +class TestGetResponseFactory(unittest.TestCase): + def test_get_factory(self): + from pyramid.registry import Registry + from pyramid.response import Response, _get_response_factory + + registry = Registry() + response = _get_response_factory(registry)(None) + self.assertTrue(isinstance(response, Response)) + + class Dummy(object): pass @@ -190,5 +201,3 @@ class DummyVenusian(object): def attach(self, wrapped, fn, category=None): self.attached.append((wrapped, fn, category)) - - diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index ba128eede..ac5ea0683 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -619,17 +619,6 @@ class TestActionInfo(unittest.TestCase): "Line 0 of file filename:\n linerepr ") -class TestGetResponseFactory(unittest.TestCase): - def test_get_factory(self): - from pyramid.util import _get_response_factory - from pyramid.registry import Registry - from pyramid.response import Response - - registry = Registry() - response = _get_response_factory(registry)(None) - self.assertTrue(isinstance(response, Response)) - - def dummyfunc(): pass class Dummy(object): diff --git a/pyramid/util.py b/pyramid/util.py index 7aecca19c..4ca2937a1 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -555,15 +555,3 @@ def action_method(wrapped): functools.update_wrapper(wrapper, wrapped) wrapper.__docobj__ = wrapped return wrapper - - -def _get_response_factory(registry): - """ Obtain a :class: `pyramid.response.Response` using the - `pyramid.interfaces.IResponseFactory`. - """ - response_factory = registry.queryUtility( - IResponseFactory, - default=lambda r: Response() - ) - - return response_factory -- cgit v1.2.3 From e094ab229eed6f8bf9e7a6a4d4406faefece41e4 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 1 Jan 2015 20:26:22 -0800 Subject: Added py3 support --- pyramid/tests/test_config/test_factories.py | 6 +++++- pyramid/util.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index 5ae486c4b..e93ba6908 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -120,13 +120,17 @@ class TestFactoriesMixin(unittest.TestCase): def boomshaka(r): pass name = text_(b'La Pe\xc3\xb1a', 'utf-8') config.add_request_method(boomshaka, name=name) + + name2 = b'La Pe\xc3\xb1a' + config.add_request_method(boomshaka, name=name2) + exts = config.registry.getUtility(IRequestExtensions) inst = InstancePropertyMixin() def set_extensions(): inst._set_extensions(exts) + self.assertRaises(ValueError, set_extensions) - self.assertTrue(name in exts.methods) class TestDeprecatedFactoriesMixinMethods(unittest.TestCase): def setUp(self): diff --git a/pyramid/util.py b/pyramid/util.py index e9f5760a6..6ab621fd4 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -113,7 +113,7 @@ class InstancePropertyMixin(object): method = fn.__get__(self, self.__class__) try: setattr(self, name, method) - except UnicodeEncodeError: + except (UnicodeEncodeError, TypeError): msg = ( '`name="%s"` is invalid. `name` must be ascii because it is ' 'used on __name__ of the method' -- cgit v1.2.3 From 22c836ecbc6f10c4851d88017243f91e469016aa Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 1 Jan 2015 22:17:13 -0800 Subject: Updated the docs to talk about `--format` --- docs/narr/commandline.rst | 45 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index 4f16617c4..02bb6138e 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -312,24 +312,49 @@ For example: :linenos: $ $VENV/bin/proutes development.ini - Name Pattern View - ---- ------- ---- - home / - home2 / - another /another None - static/ static/*subpath - catchall /*subpath - -``proutes`` generates a table with three columns: *Name*, *Pattern*, + Name Pattern View + ---- ------- ---- + debugtoolbar /_debug_toolbar/*subpath * + __static/ /static/*subpath dummy_starter:static/ * + __static2/ /static2/*subpath /var/www/static/ * + __pdt_images/ /pdt_images/*subpath pyramid_debugtoolbar:static/img/ * + a / * + no_view_attached / * + route_and_view_attached / app1.standard_views.route_and_view_attached * + method_conflicts /conflicts app1.standard_conflicts + multiview /multiview app1.standard_views.multiview GET,PATCH + not_post /not_post app1.standard_views.multview !POST,* + +``proutes`` generates a table with three columns: *Name*, *Pattern*, *Method*, and *View*. The items listed in the Name column are route names, the items listed in the Pattern column are route patterns, and the items listed in the View column are representations of the view callable that will be invoked when a request matches the associated -route pattern. The view column may show ``None`` if no associated view +route pattern. The view column may show ```` if no associated view callable could be found. If no routes are configured within your application, nothing will be printed to the console when ``proutes`` is executed. +It is convenient when using the ``proutes`` often to configure which columns +and the order you would like to view them. To facilitate this, ``proutes`` will +look for a special ``[proutes]`` section in your INI file and use those as +defaults. + +For example you may remove request method and place the view first: + +.. code-block:: text + :linenos: + + [proutes] + format = view + name + pattern + +If you want to temporarily configure the columns and order there is the +``--format` which is a comma separated list of columns you want to include. The +current available formats are ``name``, ``pattern``, ``view``, and ``method``. + + .. index:: pair: tweens; printing single: ptweens -- cgit v1.2.3 From 149ea94f24479889554b863c40fe72912f7203a8 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 1 Jan 2015 22:23:06 -0800 Subject: Updated the CHANGES to discuss these improvments. --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 129ce4616..2e2637ef1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -64,6 +64,12 @@ Features a factory that will return a custom ``Response`` class. See https://github.com/Pylons/pyramid/pull/1499 +- Overally improvments for the ``proutes`` command. Added ``--format`` and + ``--glob`` arguments to the command, introduced the ``method`` + column for displaying available request methods, and improved the ``view`` + output by showing the module instead of just ``__repr__``. + See https://github.com/Pylons/pyramid/pull/1488 + Bug Fixes --------- -- cgit v1.2.3 From 3f8ac549fa352b54a9f151b5047cc4d13f942a4a Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 1 Jan 2015 22:24:09 -0800 Subject: Fix typo --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 2e2637ef1..2079d5cff 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -64,7 +64,7 @@ Features a factory that will return a custom ``Response`` class. See https://github.com/Pylons/pyramid/pull/1499 -- Overally improvments for the ``proutes`` command. Added ``--format`` and +- Overall improvments for the ``proutes`` command. Added ``--format`` and ``--glob`` arguments to the command, introduced the ``method`` column for displaying available request methods, and improved the ``view`` output by showing the module instead of just ``__repr__``. -- cgit v1.2.3 From 83a400a3cd121fe65d33e796c28a199b35ab67e5 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 1 Jan 2015 22:25:25 -0800 Subject: Terminated the highlight on ``format`` --- docs/narr/commandline.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index 02bb6138e..aca0ff425 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -351,7 +351,7 @@ For example you may remove request method and place the view first: pattern If you want to temporarily configure the columns and order there is the -``--format` which is a comma separated list of columns you want to include. The +``--format`` which is a comma separated list of columns you want to include. The current available formats are ``name``, ``pattern``, ``view``, and ``method``. -- cgit v1.2.3 From 2d659e41d01926acaa8670c4d20be20300bcedb7 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 5 Jan 2015 23:25:58 -0600 Subject: update changes for #1417 --- CHANGES.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 129ce4616..150ca85b3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -64,6 +64,10 @@ Features a factory that will return a custom ``Response`` class. See https://github.com/Pylons/pyramid/pull/1499 +- Allow an iterator to be returned from a renderer. Previously it was only + possible to return bytes or unicode. + See https://github.com/Pylons/pyramid/pull/1417 + Bug Fixes --------- -- cgit v1.2.3 From ef2a4abb2850af8d21995f04e9f30e6a8949ff9d Mon Sep 17 00:00:00 2001 From: Pavlo Kapyshin Date: Wed, 7 Jan 2015 14:42:27 +0200 Subject: Fix "pyramid" spelling --- HISTORY.txt | 2 +- docs/narr/hybrid.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HISTORY.txt b/HISTORY.txt index 6aad221a8..242568e98 100644 --- a/HISTORY.txt +++ b/HISTORY.txt @@ -1327,7 +1327,7 @@ Bug Fixes - Make test suite pass on 32-bit systems; closes #286. closes #306. See also https://github.com/Pylons/pyramid/issues/286 -- The ``pryamid.view.view_config`` decorator did not accept a ``match_params`` +- The ``pyramid.view.view_config`` decorator did not accept a ``match_params`` predicate argument. See https://github.com/Pylons/pyramid/pull/308 - The AuthTktCookieHelper could potentially generate Unicode headers diff --git a/docs/narr/hybrid.rst b/docs/narr/hybrid.rst index 4a3258d35..1c324d22b 100644 --- a/docs/narr/hybrid.rst +++ b/docs/narr/hybrid.rst @@ -453,7 +453,7 @@ commonly in route declarations that look like this: .. code-block:: python :linenos: - from pryamid.static import static_view + from pyramid.static import static_view www = static_view('mypackage:static', use_subpath=True) -- cgit v1.2.3 From b6ad615549fb52c40f55760027bffbdd1a919aa1 Mon Sep 17 00:00:00 2001 From: Pavlo Kapyshin Date: Wed, 7 Jan 2015 14:44:27 +0200 Subject: Fix "add_subscriber" spelling --- docs/narr/introspector.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst index a7bde4cf7..0ff1615d1 100644 --- a/docs/narr/introspector.rst +++ b/docs/narr/introspector.rst @@ -121,7 +121,7 @@ introspectables in categories not described here. ``subscriber`` The subscriber callable object (the resolution of the ``subscriber`` - argument passed to ``add_susbcriber``). + argument passed to ``add_subscriber``). ``interfaces`` @@ -137,12 +137,12 @@ introspectables in categories not described here. ``predicates`` The predicate objects created as the result of passing predicate arguments - to ``add_susbcriber`` + to ``add_subscriber`` ``derived_predicates`` Wrappers around the predicate objects created as the result of passing - predicate arguments to ``add_susbcriber`` (to be used when predicates take + predicate arguments to ``add_subscriber`` (to be used when predicates take only one value but must be passed more than one). ``response adapters`` -- cgit v1.2.3 From 99d7c44610ad56bac0e90ba119b003ef11b2eb5a Mon Sep 17 00:00:00 2001 From: Pavlo Kapyshin Date: Wed, 7 Jan 2015 14:51:45 +0200 Subject: Fix rendering --- docs/narr/sessions.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst index 8da743a01..f20a36d81 100644 --- a/docs/narr/sessions.rst +++ b/docs/narr/sessions.rst @@ -44,7 +44,7 @@ It is digitally signed, however, and thus its data cannot easily be tampered with. You can configure this session factory in your :app:`Pyramid` application -by using the :meth:`pyramid.config.Configurator.set_session_factory`` method. +by using the :meth:`pyramid.config.Configurator.set_session_factory` method. .. code-block:: python :linenos: @@ -380,7 +380,7 @@ Checking CSRF Tokens Manually ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In request handling code, you can check the presence and validity of a CSRF -token with :func:`pyramid.session.check_csrf_token(request)``. If the token is +token with ``pyramid.session.check_csrf_token(request)``. If the token is valid, it will return ``True``, otherwise it will raise ``HTTPBadRequest``. Optionally, you can specify ``raises=False`` to have the check return ``False`` instead of raising an exception. -- cgit v1.2.3 From 77db3c2c000d7209d1c486585d7227181e2a4286 Mon Sep 17 00:00:00 2001 From: Pavlo Kapyshin Date: Wed, 7 Jan 2015 15:00:06 +0200 Subject: Fix typos in configuration introspection documentation --- docs/narr/introspector.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst index 0ff1615d1..8caba522c 100644 --- a/docs/narr/introspector.rst +++ b/docs/narr/introspector.rst @@ -450,9 +450,9 @@ introspectables in categories not described here. The :class:`pyramid.interfaces.IRendererInfo` object which represents this template's renderer. -``view mapper`` +``view mappers`` - Each introspectable in the ``permissions`` category represents a call to + Each introspectable in the ``view mappers`` category represents a call to :meth:`pyramid.config.Configurator.add_view` that has an explicit ``mapper`` argument to *or* a call to :meth:`pyramid.config.Configurator.set_view_mapper`; each will have @@ -481,8 +481,8 @@ introspectables in categories not described here. ``translation directories`` - Each introspectable in the ``asset overrides`` category represents an - individual element in a ``specs`` argument passed to + Each introspectable in the ``translation directories`` category represents + an individual element in a ``specs`` argument passed to :meth:`pyramid.config.Configurator.add_translation_dirs`; each will have the following data. @@ -511,7 +511,7 @@ introspectables in categories not described here. ``type`` - ``implict`` or ``explicit`` as a string. + ``implicit`` or ``explicit`` as a string. ``under`` -- cgit v1.2.3 From 3702ab07e835a06f30abf5ceb626f81114115062 Mon Sep 17 00:00:00 2001 From: Pavlo Kapyshin Date: Wed, 7 Jan 2015 15:09:26 +0200 Subject: Fix typo --- docs/narr/hooks.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index e250c2d7e..5bba0d143 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -777,7 +777,7 @@ If you want to implement your own Response object instead of using the :class:`pyramid.response.Response` object in any capacity at all, you'll have to make sure the object implements every attribute and method outlined in :class:`pyramid.interfaces.IResponse` and you'll have to ensure that it uses -``zope.interface.implementer(IResponse)`` as a class decoratoror. +``zope.interface.implementer(IResponse)`` as a class decorator. .. code-block:: python :linenos: -- cgit v1.2.3 From 47e85294779814f14e02327eb4d378197bbaeb29 Mon Sep 17 00:00:00 2001 From: Pavlo Kapyshin Date: Wed, 7 Jan 2015 19:10:31 +0200 Subject: Provide a ref to check_csrf_token --- docs/narr/sessions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst index f20a36d81..5c103405a 100644 --- a/docs/narr/sessions.rst +++ b/docs/narr/sessions.rst @@ -380,7 +380,7 @@ Checking CSRF Tokens Manually ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In request handling code, you can check the presence and validity of a CSRF -token with ``pyramid.session.check_csrf_token(request)``. If the token is +token with :func:`pyramid.session.check_csrf_token`. If the token is valid, it will return ``True``, otherwise it will raise ``HTTPBadRequest``. Optionally, you can specify ``raises=False`` to have the check return ``False`` instead of raising an exception. -- cgit v1.2.3 From 8dd970873a58e1f017c8bce7be62409b313f74e1 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 20 Jan 2015 10:44:37 -0800 Subject: Give pserve the ability top open server in browser E.g.: pserve app.ini --browser or: pserve app.ini -b --- CHANGES.txt | 3 +++ pyramid/scripts/pserve.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 150ca85b3..0a28dc248 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -68,6 +68,9 @@ Features possible to return bytes or unicode. See https://github.com/Pylons/pyramid/pull/1417 +- ``pserve`` can now take a ``-b`` or ``--browser`` option to open the server + URL in a web browser. See https://github.com/Pylons/pyramid/pull/1533 + Bug Fixes --------- diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index ea125a0dd..314efd839 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -21,9 +21,11 @@ import textwrap import threading import time import traceback +import webbrowser from paste.deploy import loadserver from paste.deploy import loadapp +from paste.deploy.loadwsgi import loadcontext, SERVER from pyramid.compat import PY3 from pyramid.compat import WIN @@ -121,6 +123,11 @@ class PServeCommand(object): dest='monitor_restart', action='store_true', help="Auto-restart server if it dies") + parser.add_option( + '-b', '--browser', + dest='browser', + action='store_true', + help="Open a web browser to server url") parser.add_option( '--status', action='store_true', @@ -334,6 +341,17 @@ class PServeCommand(object): msg = '' self.out('Exiting%s (-v to see traceback)' % msg) + if self.options.browser: + def open_browser(): + context = loadcontext(SERVER, app_spec, name=app_name, relative_to=base, + global_conf=vars) + url = 'http://{host}:{port}/'.format(**context.config()) + time.sleep(1) + webbrowser.open(url) + t = threading.Thread(target=open_browser) + t.setDaemon(True) + t.start() + serve() def loadapp(self, app_spec, name, relative_to, **kw): # pragma: no cover -- cgit v1.2.3 From 8b5000f44cddd24df111c8a1d2ff65ee6d37afbb Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 22 Jan 2015 02:46:09 -0800 Subject: move index and reference down to proper section so that docs will build on master again --- docs/narr/hooks.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 5bba0d143..17cae2c67 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -348,12 +348,6 @@ We attach and cache an object named ``extra`` to the ``request`` object. >>> request.extra.prop the property -.. index:: - single: before render event - single: adding renderer globals - -.. _beforerender_event: - .. index:: single: response factory @@ -400,6 +394,11 @@ already constructed a :term:`configurator` it can also be registered via the config = Configurator() config.set_response_factory(lambda r: MyResponse()) +.. index:: + single: before render event + single: adding renderer globals + +.. _beforerender_event: Using The Before Render Event ----------------------------- -- cgit v1.2.3 From 0e4dcf9f85babd94dcd9fc59513d257b4aba8d40 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 22 Jan 2015 03:46:27 -0800 Subject: apply changes from #1538 and #1539 --- docs/narr/logging.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/narr/logging.rst b/docs/narr/logging.rst index c16673ae6..921883091 100644 --- a/docs/narr/logging.rst +++ b/docs/narr/logging.rst @@ -254,16 +254,15 @@ level unless they're explicitly set differently. Meaning the ``myapp.views``, ``myapp.models`` (and all your app's modules') loggers by default have an effective level of ``DEBUG`` too. -For more advanced filtering, the logging module provides a `Filter -`_ object; however it cannot be used -directly from the configuration file. +For more advanced filtering, the logging module provides a +:class:`logging.Filter` object; however it cannot be used directly from the +configuration file. -Advanced Configuration +Advanced Configuration ---------------------- -To capture log output to a separate file, use a `FileHandler -`_ (or a `RotatingFileHandler -`_): +To capture log output to a separate file, use :class:`logging.FileHandler` (or +:class:`logging.handlers.RotatingFileHandler`): .. code-block:: ini @@ -317,8 +316,9 @@ output, etc., but not web traffic. For web traffic logging Paste provides the :term:`middleware`. TransLogger produces logs in the `Apache Combined Log Format `_. But TransLogger does not write to files, the Python logging system must be -configured to do this. The Python FileHandler_ logging handler can be used -alongside TransLogger to create an ``access.log`` file similar to Apache's. +configured to do this. The Python :class:`logging.FileHandler` logging +handler can be used alongside TransLogger to create an ``access.log`` file +similar to Apache's. Like any standard :term:`middleware` with a Paste entry point, TransLogger can be configured to wrap your application using ``.ini`` file syntax. First, -- cgit v1.2.3 From b8ba0f1ed25b118aeb05accb23d872b3a72dc548 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 22 Jan 2015 07:29:02 -0800 Subject: Make more ways to configure [proutes] section --- docs/narr/commandline.rst | 11 +++++ pyramid/scripts/proutes.py | 5 +- pyramid/tests/test_scripts/test_proutes.py | 73 ++++++++++++++++++++++++++++-- 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index aca0ff425..3dcb092e2 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -350,6 +350,17 @@ For example you may remove request method and place the view first: name pattern +You can also separate the formats with commas or spaces: + +.. code-block:: text + :linenos: + + [proutes] + format = view name pattern + + [proutes] + format = view, name, pattern + If you want to temporarily configure the columns and order there is the ``--format`` which is a comma separated list of columns you want to include. The current available formats are ``name``, ``pattern``, ``view``, and ``method``. diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index 2155b4983..544947724 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -2,6 +2,7 @@ import fnmatch import optparse import sys import textwrap +import re from pyramid.paster import bootstrap from pyramid.compat import (string_types, configparser) @@ -291,7 +292,8 @@ class PRoutesCommand(object): items = config.items('proutes') for k, v in items: if 'format' == k: - self.column_format = [x.strip() for x in v.split('\n')] + cols = re.split(r'[,|\s|\n]*', v) + self.column_format = [x.strip() for x in cols] except configparser.NoSectionError: return @@ -314,6 +316,7 @@ class PRoutesCommand(object): env = self.bootstrap[0](config_uri, options=parse_vars(self.args[1:])) registry = env['registry'] mapper = self._get_mapper(registry) + self.proutes_file_config(config_uri) if self.options.format: diff --git a/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py index 446d772ff..e426eee73 100644 --- a/pyramid/tests/test_scripts/test_proutes.py +++ b/pyramid/tests/test_scripts/test_proutes.py @@ -632,7 +632,7 @@ class TestPRoutesCommand(unittest.TestCase): self.assertEqual(result, 2) self.assertEqual(L[0], expected) - def test_config_format_ini(self): + def test_config_format_ini_newlines(self): from pyramid.renderers import null_renderer as nr from pyramid.config import not_ @@ -648,14 +648,79 @@ class TestPRoutesCommand(unittest.TestCase): ) command = self._makeOne() - command.options.glob = '*foo*' - command.options.format = 'method,name' + + L = [] + command.out = L.append + command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + config_factory = dummy.DummyConfigParserFactory() + command.ConfigParser = config_factory + config_factory.items = [('format', 'method\nname')] + + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = ['!POST,*', 'foo'] + + self.assertEqual(compare_to, expected) + self.assertEqual(L[0].split(), ['Method', 'Name']) + + def test_config_format_ini_spaces(self): + from pyramid.renderers import null_renderer as nr + from pyramid.config import not_ + + def view1(context, request): return 'view1' + + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b') + config.add_view( + route_name='foo', + view=view1, + renderer=nr, + request_method=not_('POST') + ) + + command = self._makeOne() + + L = [] + command.out = L.append + command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) + config_factory = dummy.DummyConfigParserFactory() + command.ConfigParser = config_factory + config_factory.items = [('format', 'method name')] + + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = ['!POST,*', 'foo'] + + self.assertEqual(compare_to, expected) + self.assertEqual(L[0].split(), ['Method', 'Name']) + + def test_config_format_ini_commas(self): + from pyramid.renderers import null_renderer as nr + from pyramid.config import not_ + + def view1(context, request): return 'view1' + + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b') + config.add_view( + route_name='foo', + view=view1, + renderer=nr, + request_method=not_('POST') + ) + + command = self._makeOne() + L = [] command.out = L.append command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),) config_factory = dummy.DummyConfigParserFactory() command.ConfigParser = config_factory - config_factory.items = [('format', 'method\name')] + config_factory.items = [('format', 'method,name')] result = command.run() self.assertEqual(result, 0) -- cgit v1.2.3 From 4fe3efda811c9d328a1a3da4acda32ecf18dbf03 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Sat, 24 Jan 2015 19:13:20 -0800 Subject: Tighten test_call_eventsends - Make it check context of events - Rename aftertraversal_events => context_found_events --- pyramid/tests/test_router.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index c6c6eea1c..30ebd5918 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -599,17 +599,19 @@ class TestRouter(unittest.TestCase): environ = self._makeEnviron() self._registerView(view, '', IViewClassifier, None, None) request_events = self._registerEventListener(INewRequest) - aftertraversal_events = self._registerEventListener(IContextFound) + context_found_events = self._registerEventListener(IContextFound) response_events = self._registerEventListener(INewResponse) router = self._makeOne() start_response = DummyStartResponse() result = router(environ, start_response) self.assertEqual(len(request_events), 1) self.assertEqual(request_events[0].request.environ, environ) - self.assertEqual(len(aftertraversal_events), 1) - self.assertEqual(aftertraversal_events[0].request.environ, environ) + self.assertEqual(len(context_found_events), 1) + self.assertEqual(context_found_events[0].request.environ, environ) + self.assertEqual(context_found_events[0].request.context, context) self.assertEqual(len(response_events), 1) self.assertEqual(response_events[0].response, response) + self.assertEqual(response_events[0].request.context, context) self.assertEqual(result, response.app_iter) def test_call_newrequest_evllist_exc_can_be_caught_by_exceptionview(self): -- cgit v1.2.3 From c7bf2744f332c0294d8d673d21b56f5edacb2eae Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 27 Jan 2015 14:51:58 -0600 Subject: fix count --- docs/narr/commandline.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index 3dcb092e2..1fe2d9278 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -325,7 +325,7 @@ For example: multiview /multiview app1.standard_views.multiview GET,PATCH not_post /not_post app1.standard_views.multview !POST,* -``proutes`` generates a table with three columns: *Name*, *Pattern*, *Method*, +``proutes`` generates a table with four columns: *Name*, *Pattern*, *Method*, and *View*. The items listed in the Name column are route names, the items listed in the Pattern column are route patterns, and the items listed in the View column are representations of the -- cgit v1.2.3 From 462af266b61e0b8aad87775926b49e8758f564e7 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Fri, 30 Jan 2015 11:36:34 -0500 Subject: Use 'functools.update_wrapper' to preserve attributes from wrapped. --- pyramid/decorator.py | 8 ++++---- pyramid/tests/test_decorator.py | 18 +++++++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/pyramid/decorator.py b/pyramid/decorator.py index 0d17bc398..df30c5e10 100644 --- a/pyramid/decorator.py +++ b/pyramid/decorator.py @@ -1,3 +1,6 @@ +import functools + + class reify(object): """ Use as a class method decorator. It operates almost exactly like the Python ``@property`` decorator, but it puts the result of the method it @@ -26,10 +29,7 @@ class reify(object): """ def __init__(self, wrapped): self.wrapped = wrapped - try: - self.__doc__ = wrapped.__doc__ - except: # pragma: no cover - pass + functools.update_wrapper(self, wrapped) def __get__(self, inst, objtype=None): if inst is None: diff --git a/pyramid/tests/test_decorator.py b/pyramid/tests/test_decorator.py index 9ab1b7229..eb5266235 100644 --- a/pyramid/tests/test_decorator.py +++ b/pyramid/tests/test_decorator.py @@ -15,15 +15,19 @@ class TestReify(unittest.TestCase): self.assertEqual(inst.__dict__['wrapped'], 'a') def test___get__noinst(self): - decorator = self._makeOne(None) + def wrapped(inst): + return 'a' + decorator = self._makeOne(wrapped) result = decorator.__get__(None) self.assertEqual(result, decorator) - def test___doc__copied(self): - def wrapped(inst): - """My doc""" - decorator = self._makeOne(wrapped) - self.assertEqual(decorator.__doc__, "My doc") - + def test_dunder_attrs_copied(self): + from pyramid.util import viewdefaults + decorator = self._makeOne(viewdefaults) + self.assertEqual(decorator.__doc__, viewdefaults.__doc__) + self.assertEqual(decorator.__name__, viewdefaults.__name__) + self.assertEqual(decorator.__module__, viewdefaults.__module__) + + class Dummy(object): pass -- cgit v1.2.3 From 7c8c852b3de053e4e8e459f3fc74e4b7d05aab93 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Fri, 30 Jan 2015 11:42:01 -0500 Subject: Suppress setuptools chatter when testing under tox. --- tox.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 714c5b6d3..29bd48639 100644 --- a/tox.ini +++ b/tox.ini @@ -4,15 +4,15 @@ envlist = [testenv] commands = - python setup.py dev - python setup.py test -q + python setup.py -q dev + python setup.py -q test -q [testenv:cover] basepython = python2.6 commands = - python setup.py dev - python setup.py nosetests --with-xunit --with-xcoverage + python setup.py -q dev + nosetests --with-xunit --with-xcoverage deps = nosexcover -- cgit v1.2.3 From 3271c7748d43fd5ecc9928ea69a8c4f497a68af5 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Fri, 30 Jan 2015 12:04:44 -0500 Subject: Ignore never-called function. Used only to allow 'functools.update_wrapper' to do its thing. --- pyramid/tests/test_decorator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/tests/test_decorator.py b/pyramid/tests/test_decorator.py index eb5266235..0a98a512d 100644 --- a/pyramid/tests/test_decorator.py +++ b/pyramid/tests/test_decorator.py @@ -16,7 +16,7 @@ class TestReify(unittest.TestCase): def test___get__noinst(self): def wrapped(inst): - return 'a' + return 'a' # pragma: no cover decorator = self._makeOne(wrapped) result = decorator.__get__(None) self.assertEqual(result, decorator) -- cgit v1.2.3 From 8da041fe67a8f8d35418681f831a57e949d605e3 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 2 Feb 2015 10:58:20 -0600 Subject: fix syntax of code-block --- pyramid/security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/security.py b/pyramid/security.py index cbb4b895f..01de80b22 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -128,7 +128,7 @@ def remember(request, userid=_marker, **kw): assumed to be a :term:`WebOb` -style :term:`response` object computed previously by the view code):: - .. code-block:: python + .. code-block: python from pyramid.security import remember headers = remember(request, 'chrism', password='123', max_age='86400') -- cgit v1.2.3 From 06ecaf50d7a66a624761043ce950ab32d3e883a9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 2 Feb 2015 14:15:27 -0600 Subject: fix syntax of code-block again --- pyramid/security.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/security.py b/pyramid/security.py index 01de80b22..011545939 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -126,9 +126,9 @@ def remember(request, userid=_marker, **kw): current :term:`authentication policy`. Common usage might look like so within the body of a view function (``response`` is assumed to be a :term:`WebOb` -style :term:`response` object - computed previously by the view code):: + computed previously by the view code): - .. code-block: python + .. code-block:: python from pyramid.security import remember headers = remember(request, 'chrism', password='123', max_age='86400') -- cgit v1.2.3 From 620bdefbff81792fd047f273a234264ce3d0a764 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 2 Feb 2015 14:17:31 -0600 Subject: add code-block to p.security.forget --- pyramid/security.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyramid/security.py b/pyramid/security.py index 011545939..f993ef353 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -170,12 +170,14 @@ def forget(request): possessed by the currently authenticated user. A common usage might look like so within the body of a view function (``response`` is assumed to be an :term:`WebOb` -style - :term:`response` object computed previously by the view code):: + :term:`response` object computed previously by the view code): - from pyramid.security import forget - headers = forget(request) - response.headerlist.extend(headers) - return response + .. code-block:: python + + from pyramid.security import forget + headers = forget(request) + response.headerlist.extend(headers) + return response If no :term:`authentication policy` is in use, this function will always return an empty sequence. -- cgit v1.2.3 From 561591252bd029981952c5c229f4cf27832d34a1 Mon Sep 17 00:00:00 2001 From: saarni Date: Thu, 5 Feb 2015 12:14:21 +0200 Subject: use getfullargspec in PY3, allowing annotations in subscribers --- CONTRIBUTORS.txt | 2 ++ pyramid/config/util.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index e4132cda5..adf2224a5 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -238,3 +238,5 @@ Contributors - Hugo Branquinho, 2014/11/25 - Adrian Teng, 2014/12/17 + +- Ilja Everila, 2015/02/05 diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 892592196..b91f3f7ab 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -22,6 +22,12 @@ ActionInfo = ActionInfo # support bw compat imports MAX_ORDER = 1 << 30 DEFAULT_PHASH = md5().hexdigest() +# support annotations and keyword-only arguments in PY3 +try: + getargspec = inspect.getfullargspec +except AttributeError: + getargspec = inspect.getargspec + def as_sorted_tuple(val): if not is_nonstr_iter(val): val = (val,) @@ -201,7 +207,7 @@ def takes_one_arg(callee, attr=None, argname=None): return False try: - argspec = inspect.getargspec(fn) + argspec = getargspec(fn) except TypeError: return False -- cgit v1.2.3 From 0ccb82204b8d04f8ffeb8b49a94fb77f981d1122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilja=20Everil=C3=A4?= Date: Thu, 5 Feb 2015 19:58:54 +0200 Subject: PY3 only test for function annotations --- pyramid/tests/test_config/test_util.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index bb61714ae..c9b0e9e9b 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -1,5 +1,5 @@ import unittest -from pyramid.compat import text_ +from pyramid.compat import text_, PY3 class TestPredicateList(unittest.TestCase): @@ -568,6 +568,14 @@ class Test_takes_one_arg(unittest.TestCase): foo = Foo() self.assertTrue(self._callFUT(foo.method)) + if PY3: + def test_function_annotations(self): + def foo(bar): + """ """ + # avoid SyntaxErrors in python2 + foo.__annotations__.update({'bar': 'baz'}) + self.assertTrue(self._callFUT(foo)) + class TestNotted(unittest.TestCase): def _makeOne(self, predicate): from pyramid.config.util import Notted -- cgit v1.2.3 From d49949081da1669914ddebb487c87edba3f41000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilja=20Everil=C3=A4?= Date: Thu, 5 Feb 2015 20:48:54 +0200 Subject: ugly nop dict update hack for PY2 and coverage --- pyramid/tests/test_config/test_util.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index c9b0e9e9b..0d0de9579 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -568,13 +568,12 @@ class Test_takes_one_arg(unittest.TestCase): foo = Foo() self.assertTrue(self._callFUT(foo.method)) - if PY3: - def test_function_annotations(self): - def foo(bar): - """ """ - # avoid SyntaxErrors in python2 - foo.__annotations__.update({'bar': 'baz'}) - self.assertTrue(self._callFUT(foo)) + def test_function_annotations(self): + def foo(bar): + """ """ + # avoid SyntaxErrors in python2, this if effectively nop + getattr(foo, '__annotations__', {}).update({'bar': 'baz'}) + self.assertTrue(self._callFUT(foo)) class TestNotted(unittest.TestCase): def _makeOne(self, predicate): -- cgit v1.2.3 From d6ff994619d18981dbde6dce7e8a10140f063e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilja=20Everil=C3=A4?= Date: Thu, 5 Feb 2015 21:01:24 +0200 Subject: remove unused import --- pyramid/tests/test_config/test_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index 0d0de9579..ccf7fa260 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -1,5 +1,5 @@ import unittest -from pyramid.compat import text_, PY3 +from pyramid.compat import text_ class TestPredicateList(unittest.TestCase): -- cgit v1.2.3 From 686f03bab3df9e1527dce1b02bcca1b05fc74bf5 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 5 Feb 2015 16:36:12 -0600 Subject: we like pep 440 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 629b77f3d..3233193e7 100644 --- a/setup.py +++ b/setup.py @@ -68,7 +68,7 @@ testing_extras = tests_require + [ ] setup(name='pyramid', - version='1.6dev', + version='1.6.dev0', description='The Pyramid Web Framework, a Pylons project', long_description=README + '\n\n' + CHANGES, classifiers=[ -- cgit v1.2.3 From 31e4924465304f8b01b8c04b22085bf24f40096e Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 5 Feb 2015 22:36:59 -0600 Subject: move getargspec import into pyramid.compat --- pyramid/compat.py | 5 +++++ pyramid/config/util.py | 7 +------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyramid/compat.py b/pyramid/compat.py index bfa345b88..5909debf2 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -244,3 +244,8 @@ else: def is_bound_method(ob): return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None +# support annotations and keyword-only arguments in PY3 +if PY3: + from inspect import getfullargspec as getargspec +else: + from inspect import getargspec diff --git a/pyramid/config/util.py b/pyramid/config/util.py index b91f3f7ab..23cdc6be8 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -3,6 +3,7 @@ import inspect from pyramid.compat import ( bytes_, + getargspec, is_nonstr_iter, ) @@ -22,12 +23,6 @@ ActionInfo = ActionInfo # support bw compat imports MAX_ORDER = 1 << 30 DEFAULT_PHASH = md5().hexdigest() -# support annotations and keyword-only arguments in PY3 -try: - getargspec = inspect.getfullargspec -except AttributeError: - getargspec = inspect.getargspec - def as_sorted_tuple(val): if not is_nonstr_iter(val): val = (val,) -- cgit v1.2.3 From 86f4d59b23f91f4a070417bebe1302661d761b2f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 5 Feb 2015 22:38:29 -0600 Subject: update changelog --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index b1bd36904..22c7a20c2 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -77,6 +77,9 @@ Features output by showing the module instead of just ``__repr__``. See https://github.com/Pylons/pyramid/pull/1488 +- Support keyword-only arguments and function annotations in views in + Python 3. See https://github.com/Pylons/pyramid/pull/1556 + Bug Fixes --------- -- cgit v1.2.3 From bbe0ad003c2f0fe5bf66953f12bd15043702838c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 5 Feb 2015 23:58:40 -0600 Subject: fix coverage --- pyramid/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/compat.py b/pyramid/compat.py index 5909debf2..c49ea1e73 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -245,7 +245,7 @@ def is_bound_method(ob): return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None # support annotations and keyword-only arguments in PY3 -if PY3: +if PY3: # pragma: no cover from inspect import getfullargspec as getargspec else: from inspect import getargspec -- cgit v1.2.3 From d7c9f0a8190b5041f8efb7f83535d181f6499605 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 14 Dec 2014 18:51:23 -0700 Subject: Move documentation for accept to non-predicate While accept is partially documented as a predicate, it is actually NOT a predicate in that the view machinery has all kinds of special cases for it. This also means that `not_` will not function on it correctly since it is not actually a predicate. --- pyramid/config/views.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index c01b72e12..8ee7ca6c9 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -841,6 +841,17 @@ class ViewsConfiguratorMixin(object): very useful for 'civilians' who are just developing stock Pyramid applications. Pay no attention to the man behind the curtain. + accept + + The value of this argument represents a match query for one + or more mimetypes in the ``Accept`` HTTP request header. If + this value is specified, it must be in one of the following + forms: a mimetype match token in the form ``text/plain``, a + wildcard mimetype match token in the form ``text/*`` or a + match-all wildcard mimetype match token in the form ``*/*``. + If any of the forms matches the ``Accept`` header of the + request, this predicate will be true. + Predicate Arguments name @@ -941,17 +952,6 @@ class ViewsConfiguratorMixin(object): This is useful for detecting AJAX requests issued from jQuery, Prototype and other Javascript libraries. - accept - - The value of this argument represents a match query for one - or more mimetypes in the ``Accept`` HTTP request header. If - this value is specified, it must be in one of the following - forms: a mimetype match token in the form ``text/plain``, a - wildcard mimetype match token in the form ``text/*`` or a - match-all wildcard mimetype match token in the form ``*/*``. - If any of the forms matches the ``Accept`` header of the - request, this predicate will be true. - header This value represents an HTTP header name or a header -- cgit v1.2.3 From 63366c3ddf353bbd2a237875f0c42e8f470c31c7 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 14 Dec 2014 18:56:33 -0700 Subject: Update CHANGES --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 22c7a20c2..b2f56e02e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -130,6 +130,12 @@ Deprecations Docs ---- +- Moved the documentation for ``accept`` on ``Configurator.add_view`` to no + longer be part of the decorator list. See + https://github.com/Pylons/pyramid/issues/1391 for a bug report stating + ``not_`` was failing on ``accept``. Discussion with @mcdonc led to the + conclusion that it should not be documented as a predicate. + - Removed logging configuration from Quick Tutorial ini files except for scaffolding- and logging-related chapters to avoid needing to explain it too early. -- cgit v1.2.3 From 958c4c7de27b3353dab6e2a7552a17321236c138 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 14 Dec 2014 19:21:55 -0700 Subject: Update add_route accept to non-predicate --- pyramid/config/routes.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index 509955cdd..c6e8fe563 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -138,6 +138,19 @@ class RoutesConfiguratorMixin(object): .. versionadded:: 1.1 + accept + + This value represents a match query for one or more + mimetypes in the ``Accept`` HTTP request header. If this + value is specified, it must be in one of the following + forms: a mimetype match token in the form ``text/plain``, a + wildcard mimetype match token in the form ``text/*`` or a + match-all wildcard mimetype match token in the form ``*/*``. + If any of the forms matches the ``Accept`` header of the + request, or if the ``Accept`` header isn't set at all in the + request, this predicate will be true. If this predicate + returns ``False``, route matching continues. + Predicate Arguments pattern @@ -220,19 +233,6 @@ class RoutesConfiguratorMixin(object): case of the header name is not significant. If this predicate returns ``False``, route matching continues. - accept - - This value represents a match query for one or more - mimetypes in the ``Accept`` HTTP request header. If this - value is specified, it must be in one of the following - forms: a mimetype match token in the form ``text/plain``, a - wildcard mimetype match token in the form ``text/*`` or a - match-all wildcard mimetype match token in the form ``*/*``. - If any of the forms matches the ``Accept`` header of the - request, or if the ``Accept`` header isn't set at all in the - request, this predicate will be true. If this predicate - returns ``False``, route matching continues. - effective_principals If specified, this value should be a :term:`principal` identifier or -- cgit v1.2.3 From c015da76f51304a5186909f01a7850de073eabdc Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 14 Dec 2014 19:56:25 -0700 Subject: Fix typo --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index b2f56e02e..d3788afa5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -131,7 +131,7 @@ Docs ---- - Moved the documentation for ``accept`` on ``Configurator.add_view`` to no - longer be part of the decorator list. See + longer be part of the predicate list. See https://github.com/Pylons/pyramid/issues/1391 for a bug report stating ``not_`` was failing on ``accept``. Discussion with @mcdonc led to the conclusion that it should not be documented as a predicate. -- cgit v1.2.3 From 42075618568881a36d9fb03812a0e651d1c084ce Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 00:06:01 -0600 Subject: fix typo --- pyramid/config/adapters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py index f6a652e3d..3d11980da 100644 --- a/pyramid/config/adapters.py +++ b/pyramid/config/adapters.py @@ -143,7 +143,7 @@ class AdaptersConfiguratorMixin(object): Adds a subscriber predicate factory. The associated subscriber predicate can later be named as a keyword argument to :meth:`pyramid.config.Configurator.add_subscriber` in the - ``**predicates`` anonyous keyword argument dictionary. + ``**predicates`` anonymous keyword argument dictionary. ``name`` should be the name of the predicate. It must be a valid Python identifier (it will be used as a ``**predicates`` keyword -- cgit v1.2.3 From da1f208369607a755be4ad355df07fac0bc7719d Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 5 Feb 2015 23:12:36 -0700 Subject: Remove all signs of predicate While this is somewhat a predicate, it really isn't for all intents and purposes because it is treated special. Make sure we document it that way. --- pyramid/config/routes.py | 19 +++++++++---------- pyramid/config/views.py | 17 +++++++++-------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index c6e8fe563..24f38a4fd 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -140,16 +140,15 @@ class RoutesConfiguratorMixin(object): accept - This value represents a match query for one or more - mimetypes in the ``Accept`` HTTP request header. If this - value is specified, it must be in one of the following - forms: a mimetype match token in the form ``text/plain``, a - wildcard mimetype match token in the form ``text/*`` or a - match-all wildcard mimetype match token in the form ``*/*``. - If any of the forms matches the ``Accept`` header of the - request, or if the ``Accept`` header isn't set at all in the - request, this predicate will be true. If this predicate - returns ``False``, route matching continues. + This value represents a match query for one or more mimetypes in the + ``Accept`` HTTP request header. If this value is specified, it must + be in one of the following forms: a mimetype match token in the form + ``text/plain``, a wildcard mimetype match token in the form + ``text/*`` or a match-all wildcard mimetype match token in the form + ``*/*``. If any of the forms matches the ``Accept`` header of the + request, or if the ``Accept`` header isn't set at all in the request, + this will match the current route. If this does not match the + ``Accept`` header of the request, route matching continues. Predicate Arguments diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 8ee7ca6c9..1f69d7e0b 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -843,14 +843,15 @@ class ViewsConfiguratorMixin(object): accept - The value of this argument represents a match query for one - or more mimetypes in the ``Accept`` HTTP request header. If - this value is specified, it must be in one of the following - forms: a mimetype match token in the form ``text/plain``, a - wildcard mimetype match token in the form ``text/*`` or a - match-all wildcard mimetype match token in the form ``*/*``. - If any of the forms matches the ``Accept`` header of the - request, this predicate will be true. + This value represents a match query for one or more mimetypes in the + ``Accept`` HTTP request header. If this value is specified, it must + be in one of the following forms: a mimetype match token in the form + ``text/plain``, a wildcard mimetype match token in the form + ``text/*`` or a match-all wildcard mimetype match token in the form + ``*/*``. If any of the forms matches the ``Accept`` header of the + request, or if the ``Accept`` header isn't set at all in the request, + this will match the current view. If this does not match the + ``Accept`` header of the request, view matching continues. Predicate Arguments -- cgit v1.2.3 From f176630ebd9848173e6cc748f361b4ce9acf76f3 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 5 Feb 2015 23:15:51 -0700 Subject: Add link to PR --- CHANGES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.txt b/CHANGES.txt index d3788afa5..832a2c216 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -135,6 +135,7 @@ Docs https://github.com/Pylons/pyramid/issues/1391 for a bug report stating ``not_`` was failing on ``accept``. Discussion with @mcdonc led to the conclusion that it should not be documented as a predicate. + See https://github.com/Pylons/pyramid/pull/1487 for this PR - Removed logging configuration from Quick Tutorial ini files except for scaffolding- and logging-related chapters to avoid needing to explain it too -- cgit v1.2.3 From fcb6cc082ea537b046df4b958f885f6a50b18d72 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 01:01:32 -0600 Subject: fix #1535 by avoiding the request if it's None --- pyramid/renderers.py | 24 ++++++++++++------------ pyramid/tests/test_renderers.py | 6 ++++++ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index d840cc317..3c35551ea 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -24,7 +24,7 @@ from pyramid.events import BeforeRender from pyramid.path import caller_package -from pyramid.response import Response, _get_response_factory +from pyramid.response import _get_response_factory from pyramid.threadlocal import get_current_registry # API @@ -355,19 +355,19 @@ class JSONP(JSON): ``self.param_name`` is present in request.GET; otherwise returns plain-JSON encoded string with content-type ``application/json``""" def _render(value, system): - request = system['request'] + request = system.get('request') default = self._make_default(request) val = self.serializer(value, default=default, **self.kw) - callback = request.GET.get(self.param_name) - if callback is None: - ct = 'application/json' - body = val - else: - ct = 'application/javascript' - body = '%s(%s);' % (callback, val) - response = request.response - if response.content_type == response.default_content_type: - response.content_type = ct + ct = 'application/json' + body = val + if request is not None: + callback = request.GET.get(self.param_name) + if callback is not None: + ct = 'application/javascript' + body = '%s(%s);' % (callback, val) + response = request.response + if response.content_type == response.default_content_type: + response.content_type = ct return body return _render diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 30fdef051..6d79cc291 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -602,6 +602,12 @@ class TestJSONP(unittest.TestCase): self.assertEqual(request.response.content_type, 'application/json') + def test_render_without_request(self): + renderer_factory = self._makeOne() + renderer = renderer_factory(None) + result = renderer({'a':'1'}, {}) + self.assertEqual(result, '{"a": "1"}') + class Dummy: pass -- cgit v1.2.3 From 1ef35b7194ad744f23cf2881bbf881690d680c83 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 01:04:17 -0600 Subject: update changelog --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 22c7a20c2..9b0ee90e9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -120,6 +120,11 @@ Bug Fixes - Fix route generation for static view asset specifications having no path. See https://github.com/Pylons/pyramid/pull/1377 +- Allow the ``pyramid.renderers.JSONP`` renderer to work even if there is no + valid request object. In this case it will not wrap the object in a + callback and thus behave just like the ``pyramid.renderers.JSON` renderer. + See https://github.com/Pylons/pyramid/pull/1561 + Deprecations ------------ -- cgit v1.2.3 From dd22319b1b8df9b1772451035fca582bd666e218 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 01:56:27 -0600 Subject: update render_to_response to prevent renderers from mutating request.response fixes #1536 --- pyramid/renderers.py | 43 +++++++++++++++++++++++++++-------------- pyramid/tests/test_renderers.py | 25 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 3c35551ea..c2be8c2eb 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -1,3 +1,4 @@ +import contextlib import json import os @@ -73,20 +74,8 @@ def render(renderer_name, value, request=None, package=None): helper = RendererHelper(name=renderer_name, package=package, registry=registry) - saved_response = None - # save the current response, preventing the renderer from affecting it - attrs = request.__dict__ if request is not None else {} - if 'response' in attrs: - saved_response = attrs['response'] - del attrs['response'] - - result = helper.render(value, None, request=request) - - # restore the original response, overwriting any changes - if saved_response is not None: - attrs['response'] = saved_response - elif 'response' in attrs: - del attrs['response'] + with temporary_response(request): + result = helper.render(value, None, request=request) return result @@ -134,7 +123,31 @@ def render_to_response(renderer_name, value, request=None, package=None): package = caller_package() helper = RendererHelper(name=renderer_name, package=package, registry=registry) - return helper.render_to_response(value, None, request=request) + + with temporary_response(request): + result = helper.render_to_response(value, None, request=request) + + return result + +@contextlib.contextmanager +def temporary_response(request): + """ + Temporarily delete request.response and restore it afterward. + """ + saved_response = None + # save the current response, preventing the renderer from affecting it + attrs = request.__dict__ if request is not None else {} + if 'response' in attrs: + saved_response = attrs['response'] + del attrs['response'] + + yield + + # restore the original response, overwriting any changes + if saved_response is not None: + attrs['response'] = saved_response + elif 'response' in attrs: + del attrs['response'] def get_renderer(renderer_name, package=None): """ Return the renderer object for the renderer ``renderer_name``. diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 6d79cc291..31e9d14f8 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -554,6 +554,31 @@ class Test_render_to_response(unittest.TestCase): renderer.assert_(a=1) renderer.assert_(request=request) + def test_response_preserved(self): + request = testing.DummyRequest() + response = object() # should error if mutated + request.response = response + # use a json renderer, which will mutate the response + result = self._callFUT('json', dict(a=1), request=request) + self.assertEqual(result.body, b'{"a": 1}') + self.assertNotEqual(request.response, result) + self.assertEqual(request.response, response) + + def test_no_response_to_preserve(self): + from pyramid.decorator import reify + class DummyRequestWithClassResponse(object): + _response = DummyResponse() + _response.content_type = None + _response.default_content_type = None + @reify + def response(self): + return self._response + request = DummyRequestWithClassResponse() + # use a json renderer, which will mutate the response + result = self._callFUT('json', dict(a=1), request=request) + self.assertEqual(result.body, b'{"a": 1}') + self.assertFalse('response' in request.__dict__) + class Test_get_renderer(unittest.TestCase): def setUp(self): self.config = testing.setUp() -- cgit v1.2.3 From d23e6986b1122e8d7344f2c882ddd3e3f423e30f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 02:06:07 -0600 Subject: update changelog and docs --- CHANGES.txt | 10 ++++++++++ pyramid/renderers.py | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b334f5258..30f30cec7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -80,6 +80,16 @@ Features - Support keyword-only arguments and function annotations in views in Python 3. See https://github.com/Pylons/pyramid/pull/1556 +- ``request.response`` will no longer be mutated when using the + ``pyramid.renderers.render_to_response()`` API. Almost all renderers + mutate the ``request.response`` response object (for example, the JSON + renderer sets ``request.response.content_type`` to ``application/json``). + However, when invoking ``render_to_response`` it is not expected that the + response object being returned would be the same one used later in the + request. The response object returned from ``render_to_response`` is now + explicitly different from ``request.response``. This does not change the + API of a renderers. See https://github.com/Pylons/pyramid/pull/1563 + Bug Fixes --------- diff --git a/pyramid/renderers.py b/pyramid/renderers.py index c2be8c2eb..42a4c98dc 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -110,9 +110,9 @@ def render_to_response(renderer_name, value, request=None, package=None): Supply a ``request`` parameter in order to provide the renderer with the most correct 'system' values (``request`` and ``context`` - in particular). Keep in mind that if the ``request`` parameter is - not passed in, any changes to ``request.response`` attributes made - before calling this function will be ignored. + in particular). Keep in mind that any changes made to ``request.response`` + prior to calling this function will not be reflected in the resulting + response object. A new response object will be created for each call. """ try: -- cgit v1.2.3 From db6280393d6f87f391b3c12a23c65fe803556286 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 02:09:20 -0600 Subject: moar docs --- pyramid/renderers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 3c35551ea..805118647 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -125,6 +125,11 @@ def render_to_response(renderer_name, value, request=None, package=None): not passed in, any changes to ``request.response`` attributes made before calling this function will be ignored. + .. versionchanged:: 1.6 + In previous versions, any changes made to ``request.response`` outside + of this function call would affect the returned response. This is no + longer the case. + """ try: registry = request.registry -- cgit v1.2.3 From 803ea0bf2d2c2d0354cc5d89fe627bc87c326081 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 02:09:37 -0600 Subject: Revert "moar docs" This reverts commit db6280393d6f87f391b3c12a23c65fe803556286. --- pyramid/renderers.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 805118647..3c35551ea 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -125,11 +125,6 @@ def render_to_response(renderer_name, value, request=None, package=None): not passed in, any changes to ``request.response`` attributes made before calling this function will be ignored. - .. versionchanged:: 1.6 - In previous versions, any changes made to ``request.response`` outside - of this function call would affect the returned response. This is no - longer the case. - """ try: registry = request.registry -- cgit v1.2.3 From e382164aa71731390f97db9734ce0b0bb014c78a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 02:09:20 -0600 Subject: moar docs --- pyramid/renderers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 42a4c98dc..c4ea22429 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -114,6 +114,11 @@ def render_to_response(renderer_name, value, request=None, package=None): prior to calling this function will not be reflected in the resulting response object. A new response object will be created for each call. + .. versionchanged:: 1.6 + In previous versions, any changes made to ``request.response`` outside + of this function call would affect the returned response. This is no + longer the case. + """ try: registry = request.registry -- cgit v1.2.3 From e30c3b9138605a16386a3e67d233b72cbbcfc5e8 Mon Sep 17 00:00:00 2001 From: Jeff Dairiki Date: Thu, 22 Jan 2015 11:01:32 -0800 Subject: Prevent DeprecationWarning from setuptools>=11.3 --- CHANGES.txt | 3 +++ pyramid/path.py | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b334f5258..a7138db1a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -125,6 +125,9 @@ Bug Fixes callback and thus behave just like the ``pyramid.renderers.JSON` renderer. See https://github.com/Pylons/pyramid/pull/1561 +- Prevent "parameters to load are deprecated" ``DeprecationWarning`` + from setuptools>=11.3. + Deprecations ------------ diff --git a/pyramid/path.py b/pyramid/path.py index 470e766f8..8eecc282b 100644 --- a/pyramid/path.py +++ b/pyramid/path.py @@ -337,8 +337,13 @@ class DottedNameResolver(Resolver): value = package.__name__ else: value = package.__name__ + value - return pkg_resources.EntryPoint.parse( - 'x=%s' % value).load(False) + # Calling EntryPoint.load with an argument is deprecated. + # See https://pythonhosted.org/setuptools/history.html#id8 + ep = pkg_resources.EntryPoint.parse('x=%s' % value) + if hasattr(ep, 'resolve'): + return ep.resolve() # setuptools>=10.2 + else: + return ep.load(False) def _zope_dottedname_style(self, value, package): """ package.module.attr style """ -- cgit v1.2.3 From c04115ab48c57d9a259e3c7f968cf71842449cdb Mon Sep 17 00:00:00 2001 From: Jeff Dairiki Date: Thu, 22 Jan 2015 14:30:25 -0800 Subject: Add NO COVER pragmas --- pyramid/path.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyramid/path.py b/pyramid/path.py index 8eecc282b..f2d8fff55 100644 --- a/pyramid/path.py +++ b/pyramid/path.py @@ -341,9 +341,10 @@ class DottedNameResolver(Resolver): # See https://pythonhosted.org/setuptools/history.html#id8 ep = pkg_resources.EntryPoint.parse('x=%s' % value) if hasattr(ep, 'resolve'): - return ep.resolve() # setuptools>=10.2 + # setuptools>=10.2 + return ep.resolve() # pragma: NO COVER else: - return ep.load(False) + return ep.load(False) # pragma: NO COVER def _zope_dottedname_style(self, value, package): """ package.module.attr style """ -- cgit v1.2.3 From b5c0ea42424abf400683baf5dbfc2c41cf049ad1 Mon Sep 17 00:00:00 2001 From: Jeff Dairiki Date: Fri, 6 Feb 2015 07:36:02 -0800 Subject: Sign CONTRIBUTORS.txt --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index adf2224a5..319d41434 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -240,3 +240,5 @@ Contributors - Adrian Teng, 2014/12/17 - Ilja Everila, 2015/02/05 + +- Geoffrey T. Dairiki, 2015/02/06 -- cgit v1.2.3 From 3279854b8ffc02593a604f84d0f72b8e7d4f24a8 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 10:05:12 -0600 Subject: update changelog for #1541 --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index a7138db1a..2dee64a84 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -126,7 +126,7 @@ Bug Fixes See https://github.com/Pylons/pyramid/pull/1561 - Prevent "parameters to load are deprecated" ``DeprecationWarning`` - from setuptools>=11.3. + from setuptools>=11.3. See https://github.com/Pylons/pyramid/pull/1541 Deprecations ------------ -- cgit v1.2.3 From 72bf6bb1b942a56a39d5ae33634e7aa8fac7080a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 13:00:59 -0600 Subject: add a respones arg to render_to_response --- pyramid/renderers.py | 14 +++++++++++--- pyramid/tests/test_renderers.py | 26 +++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index c4ea22429..088d451bb 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -79,7 +79,11 @@ def render(renderer_name, value, request=None, package=None): return result -def render_to_response(renderer_name, value, request=None, package=None): +def render_to_response(renderer_name, + value, + request=None, + package=None, + response=None): """ Using the renderer ``renderer_name`` (a template or a static renderer), render the value (or set of values) using the result of the renderer's ``__call__`` method (usually a string @@ -112,12 +116,14 @@ def render_to_response(renderer_name, value, request=None, package=None): with the most correct 'system' values (``request`` and ``context`` in particular). Keep in mind that any changes made to ``request.response`` prior to calling this function will not be reflected in the resulting - response object. A new response object will be created for each call. + response object. A new response object will be created for each call + unless one is passed as the ``response`` argument. .. versionchanged:: 1.6 In previous versions, any changes made to ``request.response`` outside of this function call would affect the returned response. This is no - longer the case. + longer the case. If you wish to send in a pre-initialized response + then you may pass one in the ``response`` argument. """ try: @@ -130,6 +136,8 @@ def render_to_response(renderer_name, value, request=None, package=None): registry=registry) with temporary_response(request): + if response is not None: + request.response = response result = helper.render_to_response(value, None, request=request) return result diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 31e9d14f8..ed6344a40 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -517,10 +517,11 @@ class Test_render_to_response(unittest.TestCase): def tearDown(self): testing.tearDown() - def _callFUT(self, renderer_name, value, request=None, package=None): + def _callFUT(self, renderer_name, value, request=None, package=None, + response=None): from pyramid.renderers import render_to_response return render_to_response(renderer_name, value, request=request, - package=package) + package=package, response=response) def test_it_no_request(self): renderer = self.config.testing_add_renderer( @@ -579,6 +580,18 @@ class Test_render_to_response(unittest.TestCase): self.assertEqual(result.body, b'{"a": 1}') self.assertFalse('response' in request.__dict__) + def test_custom_response_object(self): + class DummyRequestWithClassResponse(object): + pass + request = DummyRequestWithClassResponse() + response = DummyResponse() + # use a json renderer, which will mutate the response + result = self._callFUT('json', dict(a=1), request=request, + response=response) + self.assertTrue(result is response) + self.assertEqual(result.body, b'{"a": 1}') + self.assertFalse('response' in request.__dict__) + class Test_get_renderer(unittest.TestCase): def setUp(self): self.config = testing.setUp() @@ -639,7 +652,14 @@ class Dummy: class DummyResponse: status = '200 OK' + default_content_type = 'text/html' + content_type = default_content_type headerlist = () app_iter = () - body = '' + body = b'' + + # compat for renderer that will set unicode on py3 + def _set_text(self, val): # pragma: no cover + self.body = val.encode('utf8') + text = property(fset=_set_text) -- cgit v1.2.3 From 04206845591cb5af1038a29f6e943bfb169c2d5c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 13:03:20 -0600 Subject: update changelog --- CHANGES.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 30f30cec7..a02ae504b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -81,14 +81,18 @@ Features Python 3. See https://github.com/Pylons/pyramid/pull/1556 - ``request.response`` will no longer be mutated when using the - ``pyramid.renderers.render_to_response()`` API. Almost all renderers + ``pyramid.renderers.render_to_response()`` API. It is now necessary to + pass in a ``response=`` argument to ``render_to_response`` if you wish to + supply the renderer with a custom response object for it to use. If you + do not pass one then a response object will be created using the + application's ``IResponseFactory``. Almost all renderers mutate the ``request.response`` response object (for example, the JSON renderer sets ``request.response.content_type`` to ``application/json``). However, when invoking ``render_to_response`` it is not expected that the response object being returned would be the same one used later in the request. The response object returned from ``render_to_response`` is now explicitly different from ``request.response``. This does not change the - API of a renderers. See https://github.com/Pylons/pyramid/pull/1563 + API of a renderer. See https://github.com/Pylons/pyramid/pull/1563 Bug Fixes --------- -- cgit v1.2.3 From 20d708e89c321e5da937160beb7225c0b4fce46f Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 00:33:20 -0700 Subject: When running pcreate without scaffold, list scaffolds This fixes #1297 --- pyramid/scripts/pcreate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index edf2c39f7..a1479a2dd 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -64,7 +64,9 @@ class PCreateCommand(object): if self.options.list: return self.show_scaffolds() if not self.options.scaffold_name: - self.out('You must provide at least one scaffold name') + self.out('You must provide at least one scaffold name: -s ') + self.out('') + self.show_scaffolds() return 2 if not self.args: self.out('You must provide a project name') -- cgit v1.2.3 From c9cb19b9e14e2d1ec9ba17691212ea706f19f61c Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 00:35:36 -0700 Subject: Add changelog entry --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 832a2c216..09a4bbf88 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,11 @@ Next release Features -------- +- pcreate when run without a scaffold argument will now print information on + the missing flag, as well as a list of available scaffolds. + See https://github.com/Pylons/pyramid/pull/1566 and + https://github.com/Pylons/pyramid/issues/1297 + - Added support / testing for 'pypy3' under Tox and Travis. See https://github.com/Pylons/pyramid/pull/1469 -- cgit v1.2.3 From 665027ba49c9869abe8f0b8fe5d771c358a99e6d Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 00:41:44 -0700 Subject: Update usage line to show required -s --- pyramid/scripts/pcreate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index a1479a2dd..c634119bd 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -18,7 +18,7 @@ def main(argv=sys.argv, quiet=False): class PCreateCommand(object): verbosity = 1 # required description = "Render Pyramid scaffolding to an output directory" - usage = "usage: %prog [options] output_directory" + usage = "usage: %prog [options] -s output_directory" parser = optparse.OptionParser(usage, description=description) parser.add_option('-s', '--scaffold', dest='scaffold_name', -- cgit v1.2.3 From 0786c75a63b8d861183a08c1bf74d8afe8b929e7 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 00:42:07 -0700 Subject: Show help if missing arguments This will print the full help, followed by the available scaffolds if the user just calls pcreate without any arguments/flags at all. --- pyramid/scripts/pcreate.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index c634119bd..2d2189686 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -63,6 +63,12 @@ class PCreateCommand(object): def run(self): if self.options.list: return self.show_scaffolds() + if not self.options.scaffold_name and not self.args: + if not self.quiet: + self.parser.print_help() + self.out('') + self.show_scaffolds() + return 2 if not self.options.scaffold_name: self.out('You must provide at least one scaffold name: -s ') self.out('') -- cgit v1.2.3 From 5de795938f4ec23c53cd4678021e36a72d3188cb Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 01:06:29 -0700 Subject: Document the factory requires a positional argument --- docs/narr/hooks.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 17cae2c67..8e6cf8343 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -368,6 +368,9 @@ changed by passing a ``response_factory`` argument to the constructor of the :term:`configurator`. This argument can be either a callable or a :term:`dotted Python name` representing a callable. +The factory takes a single positional argument, which is a :term:`Request` +object. The argument may be the value ``None``. + .. code-block:: python :linenos: -- cgit v1.2.3 From 972dfae78a94ac19e97b96b36dfa91f9f7c3fed4 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 01:22:44 -0700 Subject: Fix failing test --- pyramid/tests/test_scripts/test_pcreate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/tests/test_scripts/test_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py index 020721ca7..89fdea6be 100644 --- a/pyramid/tests/test_scripts/test_pcreate.py +++ b/pyramid/tests/test_scripts/test_pcreate.py @@ -35,7 +35,7 @@ class TestPCreateCommand(unittest.TestCase): self.assertTrue(out.startswith('No scaffolds available')) def test_run_no_scaffold_name(self): - cmd = self._makeOne() + cmd = self._makeOne('dummy') result = cmd.run() self.assertEqual(result, 2) out = self.out_.getvalue() -- cgit v1.2.3 From da5f5f9ea02c2c9830c7ae016547d2bedd0e0171 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 7 Feb 2015 02:38:54 -0600 Subject: move the IResponseFactory into the public api --- docs/api/interfaces.rst | 3 +++ docs/glossary.rst | 3 ++- docs/narr/hooks.rst | 8 ++++---- pyramid/config/factories.py | 5 ++--- pyramid/interfaces.py | 11 +++++------ pyramid/util.py | 5 ----- 6 files changed, 16 insertions(+), 19 deletions(-) diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index a62976d8a..de2a664a4 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -56,6 +56,9 @@ Other Interfaces .. autointerface:: IRenderer :members: + .. autointerface:: IResponseFactory + :members: + .. autointerface:: IViewMapperFactory :members: diff --git a/docs/glossary.rst b/docs/glossary.rst index 911c22075..9c0ea8598 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -18,7 +18,8 @@ Glossary response factory An object which, provided a :term:`request` as a single positional - argument, returns a Pyramid-compatible response. + argument, returns a Pyramid-compatible response. See + :class:`pyramid.interfaces.IResponseFactory`. response An object returned by a :term:`view callable` that represents response diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 8e6cf8343..4fd7670b9 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -364,12 +364,12 @@ Whenever :app:`Pyramid` returns a response from a view it creates a object. The factory that :app:`Pyramid` uses to create a response object instance can be -changed by passing a ``response_factory`` argument to the constructor of the -:term:`configurator`. This argument can be either a callable or a -:term:`dotted Python name` representing a callable. +changed by passing a :class:`pyramid.interfaces.IResponseFactory` argument to +the constructor of the :term:`configurator`. This argument can be either a +callable or a :term:`dotted Python name` representing a callable. The factory takes a single positional argument, which is a :term:`Request` -object. The argument may be the value ``None``. +object. The argument may be ``None``. .. code-block:: python :linenos: diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index d7a48ba93..15cfb796f 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -102,9 +102,8 @@ class FactoriesConfiguratorMixin(object): """ The object passed as ``factory`` should be an object (or a :term:`dotted Python name` which refers to an object) which will be used by the :app:`Pyramid` as the default response - objects. This factory object must have the same - methods and attributes as the - :class:`pyramid.request.Response` class. + objects. The factory should conform to the + :class:`pyramid.interfaces.IResponseFactory` interface. .. note:: diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index b21c6b9cc..0f1b4efc3 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -582,12 +582,11 @@ class IStaticURLInfo(Interface): """ Generate a URL for the given path """ class IResponseFactory(Interface): - """ A utility which generates a response factory """ - def __call__(): - """ Return a response factory (e.g. a callable that returns an object - implementing IResponse, e.g. :class:`pyramid.response.Response`). It - should accept all the arguments that the Pyramid Response class - accepts.""" + """ A utility which generates a response """ + def __call__(request): + """ Return a response object implementing IResponse, + e.g. :class:`pyramid.response.Response`). It should handle the + case when ``request`` is ``None``.""" class IRequestFactory(Interface): """ A utility which generates a request """ diff --git a/pyramid/util.py b/pyramid/util.py index 4ca2937a1..18cef4602 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -15,10 +15,6 @@ from pyramid.exceptions import ( CyclicDependencyError, ) -from pyramid.interfaces import ( - IResponseFactory, - ) - from pyramid.compat import ( iteritems_, is_nonstr_iter, @@ -29,7 +25,6 @@ from pyramid.compat import ( ) from pyramid.interfaces import IActionInfo -from pyramid.response import Response from pyramid.path import DottedNameResolver as _DottedNameResolver class DottedNameResolver(_DottedNameResolver): -- cgit v1.2.3 From 9449be0316aba5b465ffb2c02a1bb0daafccd8e6 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 01:50:48 -0700 Subject: Use hammer to fix hole --- pyramid/scripts/pcreate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index 2d2189686..d2c5f8c27 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -64,7 +64,7 @@ class PCreateCommand(object): if self.options.list: return self.show_scaffolds() if not self.options.scaffold_name and not self.args: - if not self.quiet: + if not self.quiet: # pragma: no cover self.parser.print_help() self.out('') self.show_scaffolds() -- cgit v1.2.3 From 58b8adf4135656efcc063eb822e4d29f6112d329 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 01:51:04 -0700 Subject: Add test for no scaffold no project name This test at least makes sure that if there is no scaffold and no project name that the command exists with error 2 --- pyramid/tests/test_scripts/test_pcreate.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_scripts/test_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py index 89fdea6be..63e5e6368 100644 --- a/pyramid/tests/test_scripts/test_pcreate.py +++ b/pyramid/tests/test_scripts/test_pcreate.py @@ -12,10 +12,10 @@ class TestPCreateCommand(unittest.TestCase): from pyramid.scripts.pcreate import PCreateCommand return PCreateCommand - def _makeOne(self, *args): + def _makeOne(self, *args, **kw): effargs = ['pcreate'] effargs.extend(args) - cmd = self._getTargetClass()(effargs) + cmd = self._getTargetClass()(effargs, **kw) cmd.out = self.out return cmd @@ -34,6 +34,11 @@ class TestPCreateCommand(unittest.TestCase): out = self.out_.getvalue() self.assertTrue(out.startswith('No scaffolds available')) + def test_run_no_scaffold_no_args(self): + cmd = self._makeOne(quiet=True) + result = cmd.run() + self.assertEqual(result, 2) + def test_run_no_scaffold_name(self): cmd = self._makeOne('dummy') result = cmd.run() -- cgit v1.2.3 From 1e0d648503fd992323737c7c702be204337e1e36 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sat, 7 Feb 2015 12:33:03 -0800 Subject: Raise error at configuration time --- pyramid/config/factories.py | 15 +++++++++--- pyramid/tests/test_config/test_factories.py | 21 +++++++--------- pyramid/tests/test_util.py | 38 +++++++++++++++++++++++++++++ pyramid/util.py | 27 +++++++++++++------- 4 files changed, 77 insertions(+), 24 deletions(-) diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index 15cfb796f..4b2517ff1 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -15,8 +15,12 @@ from pyramid.traversal import DefaultRootFactory from pyramid.util import ( action_method, InstancePropertyMixin, + get_callable_name, ) +from pyramid.compat import native_ + + class FactoriesConfiguratorMixin(object): @action_method def set_root_factory(self, factory): @@ -33,9 +37,10 @@ class FactoriesConfiguratorMixin(object): factory = self.maybe_dotted(factory) if factory is None: factory = DefaultRootFactory + def register(): self.registry.registerUtility(factory, IRootFactory) - self.registry.registerUtility(factory, IDefaultRootFactory) # b/c + self.registry.registerUtility(factory, IDefaultRootFactory) # b/c intr = self.introspectable('root factories', None, @@ -44,7 +49,7 @@ class FactoriesConfiguratorMixin(object): intr['factory'] = factory self.action(IRootFactory, register, introspectables=(intr,)) - _set_root_factory = set_root_factory # bw compat + _set_root_factory = set_root_factory # bw compat @action_method def set_session_factory(self, factory): @@ -60,6 +65,7 @@ class FactoriesConfiguratorMixin(object): achieve the same purpose. """ factory = self.maybe_dotted(factory) + def register(): self.registry.registerUtility(factory, ISessionFactory) intr = self.introspectable('session factory', None, @@ -89,6 +95,7 @@ class FactoriesConfiguratorMixin(object): can be used to achieve the same purpose. """ factory = self.maybe_dotted(factory) + def register(): self.registry.registerUtility(factory, IRequestFactory) intr = self.introspectable('request factory', None, @@ -173,6 +180,8 @@ class FactoriesConfiguratorMixin(object): callable, name=name, reify=reify) elif name is None: name = callable.__name__ + else: + name = get_callable_name(name) def register(): exts = self.registry.queryUtility(IRequestExtensions) @@ -224,9 +233,9 @@ class FactoriesConfiguratorMixin(object): 'set_request_propery() is deprecated as of Pyramid 1.5; use ' 'add_request_method() with the property=True argument instead') + @implementer(IRequestExtensions) class _RequestExtensions(object): def __init__(self): self.descriptors = {} self.methods = {} - diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index 35677a91b..42bb5accc 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -128,24 +128,21 @@ class TestFactoriesMixin(unittest.TestCase): def test_add_request_method_with_text_type_name(self): from pyramid.interfaces import IRequestExtensions - from pyramid.compat import text_ - from pyramid.util import InstancePropertyMixin + from pyramid.compat import text_, PY3 + from pyramid.exceptions import ConfigurationError config = self._makeOne(autocommit=True) def boomshaka(r): pass - name = text_(b'La Pe\xc3\xb1a', 'utf-8') - config.add_request_method(boomshaka, name=name) - name2 = b'La Pe\xc3\xb1a' - config.add_request_method(boomshaka, name=name2) + def get_bad_name(): + if PY3: # pragma: nocover + name = b'La Pe\xc3\xb1a' + else: # pragma: nocover + name = text_(b'La Pe\xc3\xb1a', 'utf-8') - exts = config.registry.getUtility(IRequestExtensions) - inst = InstancePropertyMixin() - - def set_extensions(): - inst._set_extensions(exts) + config.add_request_method(boomshaka, name=name) - self.assertRaises(ValueError, set_extensions) + self.assertRaises(ConfigurationError, get_bad_name) class TestDeprecatedFactoriesMixinMethods(unittest.TestCase): def setUp(self): diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index ac5ea0683..405fe927a 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -124,6 +124,21 @@ class Test_InstancePropertyMixin(unittest.TestCase): self.assertEqual(1, foo.x) self.assertEqual(2, foo.y) + def test__make_property_unicode(self): + from pyramid.compat import text_ + from pyramid.exceptions import ConfigurationError + + cls = self._getTargetClass() + if PY3: # pragma: nocover + name = b'La Pe\xc3\xb1a' + else: # pragma: nocover + name = text_(b'La Pe\xc3\xb1a', 'utf-8') + + def make_bad_name(): + cls._make_property(lambda x: 1, name=name, reify=True) + + self.assertRaises(ConfigurationError, make_bad_name) + def test__set_properties_with_dict(self): foo = self._makeOne() x_name, x_fn = foo._make_property(lambda _: 1, name='x', reify=True) @@ -619,7 +634,30 @@ class TestActionInfo(unittest.TestCase): "Line 0 of file filename:\n linerepr ") +class TestCallableName(unittest.TestCase): + def test_valid_ascii(self): + from pyramid.util import get_callable_name + name = u'hello world' + self.assertEquals(get_callable_name(name), name) + + def test_invalid_ascii(self): + from pyramid.util import get_callable_name + from pyramid.compat import text_, PY3 + from pyramid.exceptions import ConfigurationError + + def get_bad_name(): + if PY3: # pragma: nocover + name = b'La Pe\xc3\xb1a' + else: # pragma: nocover + name = text_(b'La Pe\xc3\xb1a', 'utf-8') + + get_callable_name(name) + + self.assertRaises(ConfigurationError, get_bad_name) + + def dummyfunc(): pass + class Dummy(object): pass diff --git a/pyramid/util.py b/pyramid/util.py index c036c1c2e..7e8535aaf 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -22,6 +22,7 @@ from pyramid.compat import ( string_types, text_, PY3, + native_ ) from pyramid.interfaces import IActionInfo @@ -55,7 +56,7 @@ class InstancePropertyMixin(object): raise ValueError('cannot reify a property') elif name is not None: fn = lambda this: callable(this) - fn.__name__ = name + fn.__name__ = get_callable_name(name) fn.__doc__ = callable.__doc__ else: name = callable.__name__ @@ -111,14 +112,7 @@ class InstancePropertyMixin(object): def _set_extensions(self, extensions): for name, fn in iteritems_(extensions.methods): method = fn.__get__(self, self.__class__) - try: - setattr(self, name, method) - except (UnicodeEncodeError, TypeError): - msg = ( - '`name="%s"` is invalid. `name` must be ascii because it is ' - 'used on __name__ of the method' - ) - raise ValueError(msg % name) + setattr(self, name, method) self._set_properties(extensions.descriptors) @@ -558,3 +552,18 @@ def action_method(wrapped): functools.update_wrapper(wrapper, wrapped) wrapper.__docobj__ = wrapped return wrapper + + +def get_callable_name(name): + """ + Verifies that the ``name`` is ascii and will raise a ``ConfigurationError`` + if it is not. + """ + try: + return native_(name, 'ascii') + except (UnicodeEncodeError, UnicodeDecodeError): + msg = ( + '`name="%s"` is invalid. `name` must be ascii because it is ' + 'used on __name__ of the method' + ) + raise ConfigurationError(msg % name) -- cgit v1.2.3 From 4a86b211fe7d294d2c598b42bc80e0c150a08443 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sat, 7 Feb 2015 12:34:31 -0800 Subject: Remove `native_` import, not used anymore --- pyramid/config/factories.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index 4b2517ff1..10678df55 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -18,8 +18,6 @@ from pyramid.util import ( get_callable_name, ) -from pyramid.compat import native_ - class FactoriesConfiguratorMixin(object): @action_method -- cgit v1.2.3 From fd840237d4eb374c0d3f4ac2bb394aefaa43d40c Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sat, 7 Feb 2015 17:33:39 -0800 Subject: Fix py32 support --- pyramid/tests/test_util.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 405fe927a..371cd8703 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -1,9 +1,11 @@ import unittest from pyramid.compat import PY3 + class Test_InstancePropertyMixin(unittest.TestCase): def _makeOne(self): cls = self._getTargetClass() + class Foo(cls): pass return Foo() @@ -637,8 +639,14 @@ class TestActionInfo(unittest.TestCase): class TestCallableName(unittest.TestCase): def test_valid_ascii(self): from pyramid.util import get_callable_name - name = u'hello world' - self.assertEquals(get_callable_name(name), name) + from pyramid.compat import text_, PY3 + + if PY3: # pragma: nocover + name = b'hello world' + else: # pragma: nocover + name = text_(b'hello world', 'utf-8') + + self.assertEquals(get_callable_name(name), 'hello world') def test_invalid_ascii(self): from pyramid.util import get_callable_name -- cgit v1.2.3 From b809c72a6fc6d286373dea1fcfe6f674efea24a5 Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Tue, 10 Feb 2015 13:36:25 -0500 Subject: Prevent timing attacks when checking CSRF token --- pyramid/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/session.py b/pyramid/session.py index a95c3f258..29ffcfc2a 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -126,7 +126,7 @@ def check_csrf_token(request, .. versionadded:: 1.4a2 """ supplied_token = request.params.get(token, request.headers.get(header)) - if supplied_token != request.session.get_csrf_token(): + if strings_differ(request.session.get_csrf_token(), supplied_token): if raises: raise BadCSRFToken('check_csrf_token(): Invalid token') return False -- cgit v1.2.3 From 9756f6111b06de79306d3769edd83f6735275701 Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Tue, 10 Feb 2015 13:46:33 -0500 Subject: Default to an empty string instead of None --- pyramid/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/session.py b/pyramid/session.py index 29ffcfc2a..c4cfc1949 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -125,7 +125,7 @@ def check_csrf_token(request, .. versionadded:: 1.4a2 """ - supplied_token = request.params.get(token, request.headers.get(header)) + supplied_token = request.params.get(token, request.headers.get(header, "")) if strings_differ(request.session.get_csrf_token(), supplied_token): if raises: raise BadCSRFToken('check_csrf_token(): Invalid token') -- cgit v1.2.3 From b4e9902fe8cc28bac8e3e7dae0d8b2a270cf1640 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 10 Feb 2015 12:58:23 -0600 Subject: update changelog for #1574 --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 27052cf0f..1e50a623f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -133,6 +133,9 @@ Bug Fixes - Prevent "parameters to load are deprecated" ``DeprecationWarning`` from setuptools>=11.3. See https://github.com/Pylons/pyramid/pull/1541 +- Avoiding timing attacks against CSRF tokens. + See https://github.com/Pylons/pyramid/pull/1574 + Deprecations ------------ -- cgit v1.2.3 From b6e148dc678cad3bc63d64f41385114134e017be Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 11 Feb 2015 00:23:42 -0600 Subject: clone a new RendererHelper per request --- pyramid/config/views.py | 3 ++- pyramid/tests/test_config/test_views.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 1f69d7e0b..338021c24 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -348,7 +348,6 @@ class ViewDeriver(object): def _rendered_view(self, view, view_renderer): def rendered_view(context, request): - renderer = view_renderer result = view(context, request) if result.__class__ is Response: # potential common case response = result @@ -366,6 +365,8 @@ class ViewDeriver(object): name=renderer_name, package=self.kw.get('package'), registry = registry) + else: + renderer = view_renderer.clone() if '__view__' in attrs: view_inst = attrs.pop('__view__') else: diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index b0d03fb72..ed5426b12 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -2534,6 +2534,8 @@ class TestViewDeriver(unittest.TestCase): self.assertEqual(view_inst, view) self.assertEqual(ctx, context) return response + def clone(self): + return self def view(request): return 'OK' deriver = self._makeOne(renderer=moo()) @@ -2571,6 +2573,8 @@ class TestViewDeriver(unittest.TestCase): self.assertEqual(view_inst, 'view') self.assertEqual(ctx, context) return response + def clone(self): + return self def view(request): return 'OK' deriver = self._makeOne(renderer=moo()) @@ -3165,6 +3169,8 @@ class TestViewDeriver(unittest.TestCase): self.assertEqual(view_inst.__class__, View) self.assertEqual(ctx, context) return response + def clone(self): + return self class View(object): def __init__(self, context, request): pass @@ -3189,6 +3195,8 @@ class TestViewDeriver(unittest.TestCase): self.assertEqual(view_inst.__class__, View) self.assertEqual(ctx, context) return response + def clone(self): + return self class View(object): def __init__(self, request): pass @@ -3213,6 +3221,8 @@ class TestViewDeriver(unittest.TestCase): self.assertEqual(view_inst.__class__, View) self.assertEqual(ctx, context) return response + def clone(self): + return self class View: def __init__(self, context, request): pass @@ -3237,6 +3247,8 @@ class TestViewDeriver(unittest.TestCase): self.assertEqual(view_inst.__class__, View) self.assertEqual(ctx, context) return response + def clone(self): + return self class View: def __init__(self, request): pass @@ -3261,6 +3273,8 @@ class TestViewDeriver(unittest.TestCase): self.assertEqual(view_inst, view) self.assertEqual(ctx, context) return response + def clone(self): + return self class View: def index(self, context, request): return {'a':'1'} @@ -3283,6 +3297,8 @@ class TestViewDeriver(unittest.TestCase): self.assertEqual(view_inst, view) self.assertEqual(ctx, context) return response + def clone(self): + return self class View: def index(self, request): return {'a':'1'} -- cgit v1.2.3 From 06bb4a4db67321e7629f4f2438516ba358f226e9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 11 Feb 2015 00:25:53 -0600 Subject: update changelog --- CHANGES.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 2dee64a84..95566a54b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -128,6 +128,13 @@ Bug Fixes - Prevent "parameters to load are deprecated" ``DeprecationWarning`` from setuptools>=11.3. See https://github.com/Pylons/pyramid/pull/1541 +- Avoiding sharing the ``IRenderer`` objects across threads when attached to + a view using the `renderer=` argument. These renderers were instantiated + at time of first render and shared between requests, causing potentially + subtle effects like `pyramid.reload_templates = true` failing to work + in `pyramid_mako`. See https://github.com/Pylons/pyramid/pull/1575 + and https://github.com/Pylons/pyramid/issues/1268 + Deprecations ------------ -- cgit v1.2.3 From c534e00ed06ef17506cf5f74553310ec15653834 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Tue, 10 Feb 2015 22:43:39 -0800 Subject: Don't create sdist with tox (latest setuptools doesn't like mixing) --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 29bd48639..202e29e30 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,5 @@ [tox] +skipsdist = True envlist = py26,py27,py32,py33,py34,pypy,pypy3,cover -- cgit v1.2.3 From 14126cae5cf308ed466ed3eea576094e9c2193b4 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 11 Feb 2015 12:25:02 -0600 Subject: fix up tests to generate coverage reports always and combine at the end --- .gitignore | 3 +++ .travis.yml | 21 ++++++++++---------- pyramid/tests/test_config/pkgs/asset/models.py | 8 -------- pyramid/tests/test_config/pkgs/asset/views.py | 22 --------------------- pyramid/tests/test_scripts/pystartup.py | 1 - pyramid/tests/test_scripts/pystartup.txt | 3 +++ pyramid/tests/test_scripts/test_pshell.py | 2 +- setup.cfg | 1 - tox.ini | 27 ++++++++++++++------------ 9 files changed, 32 insertions(+), 56 deletions(-) delete mode 100644 pyramid/tests/test_config/pkgs/asset/models.py delete mode 100644 pyramid/tests/test_config/pkgs/asset/views.py delete mode 100644 pyramid/tests/test_scripts/pystartup.py create mode 100644 pyramid/tests/test_scripts/pystartup.txt diff --git a/.gitignore b/.gitignore index 8dca2069c..b60cd530a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,12 @@ *~ .*.swp .coverage +.coverage.* .tox/ nosetests.xml coverage.xml +nosetests-*.xml +coverage-*.xml tutorial.db build/ dist/ diff --git a/.travis.yml b/.travis.yml index e2f379802..7cf2b6a16 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,15 @@ # Wire up travis language: python -# -#env: -# - TOXENV=clean -# - TOXENV=py26 -# - TOXENV=py27 -# - TOXENV=py32 -# - TOXENV=py33 -# - TOXENV=py34 -# - TOXENV=pypy -# - TOXENV=pypy3 -# - TOXENV=report + +env: + - TOXENV=py26 + - TOXENV=py27 + - TOXENV=py32 + - TOXENV=py33 + - TOXENV=py34 + - TOXENV=pypy + - TOXENV=pypy3 + - TOXENV=py26,py32,cover install: - travis_retry pip install tox diff --git a/pyramid/tests/test_config/pkgs/asset/models.py b/pyramid/tests/test_config/pkgs/asset/models.py deleted file mode 100644 index d80d14bb3..000000000 --- a/pyramid/tests/test_config/pkgs/asset/models.py +++ /dev/null @@ -1,8 +0,0 @@ -from zope.interface import Interface - -class IFixture(Interface): - pass - -def fixture(): - """ """ - diff --git a/pyramid/tests/test_config/pkgs/asset/views.py b/pyramid/tests/test_config/pkgs/asset/views.py deleted file mode 100644 index cbfc5a574..000000000 --- a/pyramid/tests/test_config/pkgs/asset/views.py +++ /dev/null @@ -1,22 +0,0 @@ -from zope.interface import Interface -from webob import Response -from pyramid.httpexceptions import HTTPForbidden - -def fixture_view(context, request): - """ """ - return Response('fixture') - -def erroneous_view(context, request): - """ """ - raise RuntimeError() - -def exception_view(context, request): - """ """ - return Response('supressed') - -def protected_view(context, request): - """ """ - raise HTTPForbidden() - -class IDummy(Interface): - pass diff --git a/pyramid/tests/test_scripts/pystartup.py b/pyramid/tests/test_scripts/pystartup.py deleted file mode 100644 index c4e5bcc80..000000000 --- a/pyramid/tests/test_scripts/pystartup.py +++ /dev/null @@ -1 +0,0 @@ -foo = 1 diff --git a/pyramid/tests/test_scripts/pystartup.txt b/pyramid/tests/test_scripts/pystartup.txt new file mode 100644 index 000000000..c62c4ca74 --- /dev/null +++ b/pyramid/tests/test_scripts/pystartup.txt @@ -0,0 +1,3 @@ +# this file has a .txt extension to avoid coverage reports +# since it is not imported but rather the contents are read and exec'd +foo = 1 diff --git a/pyramid/tests/test_scripts/test_pshell.py b/pyramid/tests/test_scripts/test_pshell.py index a6ba2eaea..dab32fecd 100644 --- a/pyramid/tests/test_scripts/test_pshell.py +++ b/pyramid/tests/test_scripts/test_pshell.py @@ -379,7 +379,7 @@ class TestPShellCommand(unittest.TestCase): os.path.abspath( os.path.join( os.path.dirname(__file__), - 'pystartup.py'))) + 'pystartup.txt'))) shell = dummy.DummyShell() command.run(shell) self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') diff --git a/setup.cfg b/setup.cfg index bc092a6ca..875480594 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,6 @@ zip_ok = false match=^test where=pyramid nocapture=1 -cover-package=pyramid [aliases] dev = develop easy_install pyramid[testing] diff --git a/tox.ini b/tox.ini index ba0007d7a..dae31ddd6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,24 +1,27 @@ [tox] +skipsdist = True envlist = - clean,py26,py27,py32,py33,py34,pypy,pypy3,report - -[testenv:clean] -commands = coverage erase -deps = coverage + py26,py27,py32,py33,py34,pypy,pypy3,cover [testenv] commands = - python setup.py dev - python setup.py nosetests --with-coverage + {envbindir}/python setup.py dev + {envbindir}/coverage run --source={toxinidir}/pyramid {envbindir}/nosetests --xunit-file=nosetests-{envname}.xml + {envbindir}/coverage xml -o coverage-{envname}.xml +setenv = + COVERAGE_FILE=.coverage.{envname} -[testenv:report] +[testenv:cover] commands = - python setup.py dev - python setup.py nosetests --with-xunit --with-xcoverage --cover-min-percentage=100 + {envbindir}/coverage erase + {envbindir}/coverage combine + {envbindir}/coverage xml + {envbindir}/coverage report --show-missing --fail-under=100 deps = - nosexcover + coverage +setenv = + COVERAGE_FILE=.coverage # we separate coverage into its own testenv because a) "last run wins" wrt # cobertura jenkins reporting and b) pypy and jython can't handle any # combination of versions of coverage and nosexcover that i can find. - -- cgit v1.2.3 From c45d6aea833245fa4fd9bb81352feb37045dfb07 Mon Sep 17 00:00:00 2001 From: David Glick Date: Thu, 12 Feb 2015 21:10:30 -0800 Subject: Add workaround to make sure echo is enabled after reload (refs #689) Also add myself to CONTRIBUTORS.txt --- CHANGES.txt | 3 +++ CONTRIBUTORS.txt | 2 ++ pyramid/scripts/pserve.py | 14 ++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 1e50a623f..6a174bb1c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -88,6 +88,9 @@ Features Bug Fixes --------- +- Work around an issue where ``pserve --reload`` would leave terminal echo + disabled if it reloaded during a pdb session. + - ``pyramid.wsgi.wsgiapp`` and ``pyramid.wsgi.wsgiapp2`` now raise ``ValueError`` when accidentally passed ``None``. See https://github.com/Pylons/pyramid/pull/1320 diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 319d41434..4f9bd6e41 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -242,3 +242,5 @@ Contributors - Ilja Everila, 2015/02/05 - Geoffrey T. Dairiki, 2015/02/06 + +- David Glick, 2015/02/12 diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 314efd839..d2ea1719b 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -36,6 +36,11 @@ from pyramid.scripts.common import parse_vars MAXFD = 1024 +try: + import termios +except ImportError: # pragma: no cover + termios = None + if WIN and not hasattr(os, 'kill'): # pragma: no cover # py 2.6 on windows def kill(pid, sig=None): @@ -709,6 +714,14 @@ def _turn_sigterm_into_systemexit(): # pragma: no cover raise SystemExit signal.signal(signal.SIGTERM, handle_term) +def ensure_echo_on(): # pragma: no cover + if termios: + fd = sys.stdin.fileno() + attr_list = termios.tcgetattr(fd) + if not attr_list[3] & termios.ECHO: + attr_list[3] |= termios.ECHO + termios.tcsetattr(fd, termios.TCSANOW, attr_list) + def install_reloader(poll_interval=1, extra_files=None): # pragma: no cover """ Install the reloading monitor. @@ -718,6 +731,7 @@ def install_reloader(poll_interval=1, extra_files=None): # pragma: no cover ``raise_keyboard_interrupt`` option creates a unignorable signal which causes the whole application to shut-down (rudely). """ + ensure_echo_on() mon = Monitor(poll_interval=poll_interval) if extra_files is None: extra_files = [] -- cgit v1.2.3 From 9343dbc71b268cf3c4ff4ac7e164af76ce39d5ec Mon Sep 17 00:00:00 2001 From: David Glick Date: Thu, 12 Feb 2015 21:16:12 -0800 Subject: remove obsolete note about raise_keyboard_interrupt that's left over from paste --- pyramid/scripts/pserve.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 314efd839..c5e54d670 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -714,9 +714,7 @@ def install_reloader(poll_interval=1, extra_files=None): # pragma: no cover Install the reloading monitor. On some platforms server threads may not terminate when the main - thread does, causing ports to remain open/locked. The - ``raise_keyboard_interrupt`` option creates a unignorable signal - which causes the whole application to shut-down (rudely). + thread does, causing ports to remain open/locked. """ mon = Monitor(poll_interval=poll_interval) if extra_files is None: -- cgit v1.2.3 From c94c39bf9cc6a5c0fd9207046e8feb8b9a917447 Mon Sep 17 00:00:00 2001 From: David Glick Date: Thu, 12 Feb 2015 21:16:43 -0800 Subject: fix instructions for running coverage via tox --- HACKING.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HACKING.txt b/HACKING.txt index 16c17699c..e104869ec 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -195,7 +195,7 @@ Test Coverage ------------- - The codebase *must* have 100% test statement coverage after each commit. - You can test coverage via ``tox -e coverage``, or alternately by installing + You can test coverage via ``tox -e cover``, or alternately by installing ``nose`` and ``coverage`` into your virtualenv (easiest via ``setup.py dev``) , and running ``setup.py nosetests --with-coverage``. -- cgit v1.2.3 From 03d964a924e0ef183c3cd78a61c043b1f74f5570 Mon Sep 17 00:00:00 2001 From: David Glick Date: Fri, 13 Feb 2015 09:20:33 -0800 Subject: add pull request reference --- CHANGES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.txt b/CHANGES.txt index 6a174bb1c..37803b3ed 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -90,6 +90,7 @@ Bug Fixes - Work around an issue where ``pserve --reload`` would leave terminal echo disabled if it reloaded during a pdb session. + See https://github.com/Pylons/pyramid/pull/1577 - ``pyramid.wsgi.wsgiapp`` and ``pyramid.wsgi.wsgiapp2`` now raise ``ValueError`` when accidentally passed ``None``. -- cgit v1.2.3 From 04cc91a7ac2d203e5acda41aa7c4975f78171274 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 16 Feb 2015 22:09:35 -0600 Subject: add InstancePropertyHelper and apply_request_extensions --- docs/api/request.rst | 1 + pyramid/config/factories.py | 4 +- pyramid/interfaces.py | 3 +- pyramid/request.py | 26 ++++- pyramid/router.py | 3 +- pyramid/scripting.py | 8 +- pyramid/tests/test_request.py | 45 +++++++- pyramid/tests/test_router.py | 8 +- pyramid/tests/test_scripting.py | 16 ++- pyramid/tests/test_util.py | 236 +++++++++++++++++++++++++++++++--------- pyramid/util.py | 75 +++++++------ 11 files changed, 321 insertions(+), 104 deletions(-) diff --git a/docs/api/request.rst b/docs/api/request.rst index dd68fa09c..b325ad076 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -369,3 +369,4 @@ that used as ``request.GET``, ``request.POST``, and ``request.params``), see :class:`pyramid.interfaces.IMultiDict`. +.. autofunction:: apply_request_extensions(request) diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index 10678df55..f0b6252ae 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -14,8 +14,8 @@ from pyramid.traversal import DefaultRootFactory from pyramid.util import ( action_method, - InstancePropertyMixin, get_callable_name, + InstancePropertyHelper, ) @@ -174,7 +174,7 @@ class FactoriesConfiguratorMixin(object): property = property or reify if property: - name, callable = InstancePropertyMixin._make_property( + name, callable = InstancePropertyHelper.make_property( callable, name=name, reify=reify) elif name is None: name = callable.__name__ diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 0f1b4efc3..d7422bdde 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -591,8 +591,7 @@ class IResponseFactory(Interface): class IRequestFactory(Interface): """ A utility which generates a request """ def __call__(environ): - """ Return an object implementing IRequest, e.g. an instance - of ``pyramid.request.Request``""" + """ Return an instance of ``pyramid.request.Request``""" def blank(path): """ Return an empty request object (see diff --git a/pyramid/request.py b/pyramid/request.py index b2e2efe05..3cbe5d9e3 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -8,6 +8,7 @@ from webob import BaseRequest from pyramid.interfaces import ( IRequest, + IRequestExtensions, IResponse, ISessionFactory, ) @@ -16,6 +17,7 @@ from pyramid.compat import ( text_, bytes_, native_, + iteritems_, ) from pyramid.decorator import reify @@ -26,7 +28,10 @@ from pyramid.security import ( AuthorizationAPIMixin, ) from pyramid.url import URLMethodsMixin -from pyramid.util import InstancePropertyMixin +from pyramid.util import ( + InstancePropertyHelper, + InstancePropertyMixin, +) class TemplateContext(object): pass @@ -307,3 +312,22 @@ def call_app_with_subpath_as_path_info(request, app): new_request.environ['PATH_INFO'] = new_path_info return new_request.get_response(app) + +def apply_request_extensions(request, extensions=None): + """Apply request extensions (methods and properties) to an instance of + :class:`pyramid.interfaces.IRequest`. This method is dependent on the + ``request`` containing a properly initialized registry. + + After invoking this method, the ``request`` should have the methods + and properties that were defined using + :meth:`pyramid.config.Configurator.add_request_method`. + """ + if extensions is None: + extensions = request.registry.queryUtility(IRequestExtensions) + if extensions is not None: + for name, fn in iteritems_(extensions.methods): + method = fn.__get__(request, request.__class__) + setattr(request, name, method) + + InstancePropertyHelper.apply_properties( + request, extensions.descriptors) diff --git a/pyramid/router.py b/pyramid/router.py index ba4f85b18..0b1ecade7 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -27,6 +27,7 @@ from pyramid.events import ( from pyramid.exceptions import PredicateMismatch from pyramid.httpexceptions import HTTPNotFound from pyramid.request import Request +from pyramid.request import apply_request_extensions from pyramid.threadlocal import manager from pyramid.traversal import ( @@ -213,7 +214,7 @@ class Router(object): try: extensions = self.request_extensions if extensions is not None: - request._set_extensions(extensions) + apply_request_extensions(request, extensions=extensions) response = handle_request(request) if request.response_callbacks: diff --git a/pyramid/scripting.py b/pyramid/scripting.py index fdb4aa430..d9587338f 100644 --- a/pyramid/scripting.py +++ b/pyramid/scripting.py @@ -1,12 +1,12 @@ from pyramid.config import global_registries from pyramid.exceptions import ConfigurationError -from pyramid.request import Request from pyramid.interfaces import ( - IRequestExtensions, IRequestFactory, IRootFactory, ) +from pyramid.request import Request +from pyramid.request import apply_request_extensions from pyramid.threadlocal import manager as threadlocal_manager from pyramid.traversal import DefaultRootFactory @@ -77,9 +77,7 @@ def prepare(request=None, registry=None): request.registry = registry threadlocals = {'registry':registry, 'request':request} threadlocal_manager.push(threadlocals) - extensions = registry.queryUtility(IRequestExtensions) - if extensions is not None: - request._set_extensions(extensions) + apply_request_extensions(request) def closer(): threadlocal_manager.pop() root_factory = registry.queryUtility(IRootFactory, diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 48af98f59..f142e4536 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -435,7 +435,50 @@ class Test_call_app_with_subpath_as_path_info(unittest.TestCase): self.assertEqual(request.environ['SCRIPT_NAME'], '/' + encoded) self.assertEqual(request.environ['PATH_INFO'], '/' + encoded) -class DummyRequest: +class Test_apply_request_extensions(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, request, extensions=None): + from pyramid.request import apply_request_extensions + return apply_request_extensions(request, extensions=extensions) + + def test_it_with_registry(self): + from pyramid.interfaces import IRequestExtensions + extensions = Dummy() + extensions.methods = {'foo': lambda x, y: y} + extensions.descriptors = {'bar': property(lambda x: 'bar')} + self.config.registry.registerUtility(extensions, IRequestExtensions) + request = DummyRequest() + request.registry = self.config.registry + self._callFUT(request) + self.assertEqual(request.bar, 'bar') + self.assertEqual(request.foo('abc'), 'abc') + + def test_it_override_extensions(self): + from pyramid.interfaces import IRequestExtensions + ignore = Dummy() + ignore.methods = {'x': lambda x, y, z: 'asdf'} + ignore.descriptors = {'bar': property(lambda x: 'asdf')} + self.config.registry.registerUtility(ignore, IRequestExtensions) + request = DummyRequest() + request.registry = self.config.registry + + extensions = Dummy() + extensions.methods = {'foo': lambda x, y: y} + extensions.descriptors = {'bar': property(lambda x: 'bar')} + self._callFUT(request, extensions=extensions) + self.assertRaises(AttributeError, lambda: request.x) + self.assertEqual(request.bar, 'bar') + self.assertEqual(request.foo('abc'), 'abc') + +class Dummy(object): + pass + +class DummyRequest(object): def __init__(self, environ=None): if environ is None: environ = {} diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index 30ebd5918..b57c248d5 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -317,6 +317,7 @@ class TestRouter(unittest.TestCase): from pyramid.interfaces import IRequestExtensions from pyramid.interfaces import IRequest from pyramid.request import Request + from pyramid.util import InstancePropertyHelper context = DummyContext() self._registerTraverserFactory(context) class Extensions(object): @@ -324,11 +325,12 @@ class TestRouter(unittest.TestCase): self.methods = {} self.descriptors = {} extensions = Extensions() - L = [] + ext_method = lambda r: 'bar' + name, fn = InstancePropertyHelper.make_property(ext_method, name='foo') + extensions.descriptors[name] = fn request = Request.blank('/') request.request_iface = IRequest request.registry = self.registry - request._set_extensions = lambda *x: L.extend(x) def request_factory(environ): return request self.registry.registerUtility(extensions, IRequestExtensions) @@ -342,7 +344,7 @@ class TestRouter(unittest.TestCase): router.request_factory = request_factory start_response = DummyStartResponse() router(environ, start_response) - self.assertEqual(L, [extensions]) + self.assertEqual(view.request.foo, 'bar') def test_call_view_registered_nonspecific_default_path(self): from pyramid.interfaces import IViewClassifier diff --git a/pyramid/tests/test_scripting.py b/pyramid/tests/test_scripting.py index a36d1ed71..1e952062b 100644 --- a/pyramid/tests/test_scripting.py +++ b/pyramid/tests/test_scripting.py @@ -122,11 +122,15 @@ class Test_prepare(unittest.TestCase): self.assertEqual(request.context, context) def test_it_with_extensions(self): - exts = Dummy() + from pyramid.util import InstancePropertyHelper + exts = DummyExtensions() + ext_method = lambda r: 'bar' + name, fn = InstancePropertyHelper.make_property(ext_method, 'foo') + exts.descriptors[name] = fn request = DummyRequest({}) registry = request.registry = self._makeRegistry([exts, DummyFactory]) info = self._callFUT(request=request, registry=registry) - self.assertEqual(request.extensions, exts) + self.assertEqual(request.foo, 'bar') root, closer = info['root'], info['closer'] closer() @@ -199,11 +203,13 @@ class DummyThreadLocalManager: def pop(self): self.popped.append(True) -class DummyRequest: +class DummyRequest(object): matchdict = None matched_route = None def __init__(self, environ): self.environ = environ - def _set_extensions(self, exts): - self.extensions = exts +class DummyExtensions: + def __init__(self): + self.descriptors = {} + self.methods = {} diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 371cd8703..459c729a0 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -2,6 +2,188 @@ import unittest from pyramid.compat import PY3 +class Test_InstancePropertyHelper(unittest.TestCase): + def _makeOne(self): + cls = self._getTargetClass() + return cls() + + def _getTargetClass(self): + from pyramid.util import InstancePropertyHelper + return InstancePropertyHelper + + def test_callable(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker) + foo.bar = 1 + self.assertEqual(1, foo.worker) + foo.bar = 2 + self.assertEqual(2, foo.worker) + + def test_callable_with_name(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, name='x') + foo.bar = 1 + self.assertEqual(1, foo.x) + foo.bar = 2 + self.assertEqual(2, foo.x) + + def test_callable_with_reify(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, reify=True) + foo.bar = 1 + self.assertEqual(1, foo.worker) + foo.bar = 2 + self.assertEqual(1, foo.worker) + + def test_callable_with_name_reify(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, name='x') + helper.set_property(foo, worker, name='y', reify=True) + foo.bar = 1 + self.assertEqual(1, foo.y) + self.assertEqual(1, foo.x) + foo.bar = 2 + self.assertEqual(2, foo.x) + self.assertEqual(1, foo.y) + + def test_property_without_name(self): + def worker(obj): pass + foo = Dummy() + helper = self._getTargetClass() + self.assertRaises(ValueError, helper.set_property, foo, property(worker)) + + def test_property_with_name(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, property(worker), name='x') + foo.bar = 1 + self.assertEqual(1, foo.x) + foo.bar = 2 + self.assertEqual(2, foo.x) + + def test_property_with_reify(self): + def worker(obj): pass + foo = Dummy() + helper = self._getTargetClass() + self.assertRaises(ValueError, helper.set_property, + foo, property(worker), name='x', reify=True) + + def test_override_property(self): + def worker(obj): pass + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, name='x') + def doit(): + foo.x = 1 + self.assertRaises(AttributeError, doit) + + def test_override_reify(self): + def worker(obj): pass + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, name='x', reify=True) + foo.x = 1 + self.assertEqual(1, foo.x) + foo.x = 2 + self.assertEqual(2, foo.x) + + def test_reset_property(self): + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, lambda _: 1, name='x') + self.assertEqual(1, foo.x) + helper.set_property(foo, lambda _: 2, name='x') + self.assertEqual(2, foo.x) + + def test_reset_reify(self): + """ This is questionable behavior, but may as well get notified + if it changes.""" + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, lambda _: 1, name='x', reify=True) + self.assertEqual(1, foo.x) + helper.set_property(foo, lambda _: 2, name='x', reify=True) + self.assertEqual(1, foo.x) + + def test_make_property(self): + from pyramid.decorator import reify + helper = self._getTargetClass() + name, fn = helper.make_property(lambda x: 1, name='x', reify=True) + self.assertEqual(name, 'x') + self.assertTrue(isinstance(fn, reify)) + + def test_apply_properties_with_iterable(self): + foo = Dummy() + helper = self._getTargetClass() + x = helper.make_property(lambda _: 1, name='x', reify=True) + y = helper.make_property(lambda _: 2, name='y') + helper.apply_properties(foo, [x, y]) + self.assertEqual(1, foo.x) + self.assertEqual(2, foo.y) + + def test_apply_properties_with_dict(self): + foo = Dummy() + helper = self._getTargetClass() + x_name, x_fn = helper.make_property(lambda _: 1, name='x', reify=True) + y_name, y_fn = helper.make_property(lambda _: 2, name='y') + helper.apply_properties(foo, {x_name: x_fn, y_name: y_fn}) + self.assertEqual(1, foo.x) + self.assertEqual(2, foo.y) + + def test_make_property_unicode(self): + from pyramid.compat import text_ + from pyramid.exceptions import ConfigurationError + + cls = self._getTargetClass() + if PY3: # pragma: nocover + name = b'La Pe\xc3\xb1a' + else: # pragma: nocover + name = text_(b'La Pe\xc3\xb1a', 'utf-8') + + def make_bad_name(): + cls.make_property(lambda x: 1, name=name, reify=True) + + self.assertRaises(ConfigurationError, make_bad_name) + + def test_add_property(self): + helper = self._makeOne() + helper.add_property(lambda obj: obj.bar, name='x', reify=True) + helper.add_property(lambda obj: obj.bar, name='y') + self.assertEqual(len(helper.properties), 2) + foo = Dummy() + helper.apply(foo) + foo.bar = 1 + self.assertEqual(foo.x, 1) + self.assertEqual(foo.y, 1) + foo.bar = 2 + self.assertEqual(foo.x, 1) + self.assertEqual(foo.y, 2) + + def test_apply_multiple_times(self): + helper = self._makeOne() + helper.add_property(lambda obj: 1, name='x') + foo, bar = Dummy(), Dummy() + helper.apply(foo) + self.assertEqual(foo.x, 1) + helper.add_property(lambda obj: 2, name='x') + helper.apply(bar) + self.assertEqual(foo.x, 1) + self.assertEqual(bar.x, 2) + class Test_InstancePropertyMixin(unittest.TestCase): def _makeOne(self): cls = self._getTargetClass() @@ -111,58 +293,6 @@ class Test_InstancePropertyMixin(unittest.TestCase): foo.set_property(lambda _: 2, name='x', reify=True) self.assertEqual(1, foo.x) - def test__make_property(self): - from pyramid.decorator import reify - cls = self._getTargetClass() - name, fn = cls._make_property(lambda x: 1, name='x', reify=True) - self.assertEqual(name, 'x') - self.assertTrue(isinstance(fn, reify)) - - def test__set_properties_with_iterable(self): - foo = self._makeOne() - x = foo._make_property(lambda _: 1, name='x', reify=True) - y = foo._make_property(lambda _: 2, name='y') - foo._set_properties([x, y]) - self.assertEqual(1, foo.x) - self.assertEqual(2, foo.y) - - def test__make_property_unicode(self): - from pyramid.compat import text_ - from pyramid.exceptions import ConfigurationError - - cls = self._getTargetClass() - if PY3: # pragma: nocover - name = b'La Pe\xc3\xb1a' - else: # pragma: nocover - name = text_(b'La Pe\xc3\xb1a', 'utf-8') - - def make_bad_name(): - cls._make_property(lambda x: 1, name=name, reify=True) - - self.assertRaises(ConfigurationError, make_bad_name) - - def test__set_properties_with_dict(self): - foo = self._makeOne() - x_name, x_fn = foo._make_property(lambda _: 1, name='x', reify=True) - y_name, y_fn = foo._make_property(lambda _: 2, name='y') - foo._set_properties({x_name: x_fn, y_name: y_fn}) - self.assertEqual(1, foo.x) - self.assertEqual(2, foo.y) - - def test__set_extensions(self): - inst = self._makeOne() - def foo(self, result): - return result - n, bar = inst._make_property(lambda _: 'bar', name='bar') - class Extensions(object): - def __init__(self): - self.methods = {'foo':foo} - self.descriptors = {'bar':bar} - extensions = Extensions() - inst._set_extensions(extensions) - self.assertEqual(inst.bar, 'bar') - self.assertEqual(inst.foo('abc'), 'abc') - class Test_WeakOrderedSet(unittest.TestCase): def _makeOne(self): from pyramid.config import WeakOrderedSet @@ -646,7 +776,7 @@ class TestCallableName(unittest.TestCase): else: # pragma: nocover name = text_(b'hello world', 'utf-8') - self.assertEquals(get_callable_name(name), 'hello world') + self.assertEqual(get_callable_name(name), 'hello world') def test_invalid_ascii(self): from pyramid.util import get_callable_name diff --git a/pyramid/util.py b/pyramid/util.py index 7e8535aaf..63d113361 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -34,14 +34,21 @@ class DottedNameResolver(_DottedNameResolver): _marker = object() -class InstancePropertyMixin(object): - """ Mixin that will allow an instance to add properties at - run-time as if they had been defined via @property or @reify - on the class itself. +class InstancePropertyHelper(object): + """A helper object for assigning properties and descriptors to instances. + It is not normally possible to do this because descriptors must be + defined on the class itself. + + This class is optimized for adding multiple properties at once to an + instance. This is done by calling :meth:`.add_property` once + per-property and then invoking :meth:`.apply` on target objects. + """ + def __init__(self): + self.properties = {} @classmethod - def _make_property(cls, callable, name=None, reify=False): + def make_property(cls, callable, name=None, reify=False): """ Convert a callable into one suitable for adding to the instance. This will return a 2-tuple containing the computed (name, property) pair. @@ -69,25 +76,12 @@ class InstancePropertyMixin(object): return name, fn - def _set_properties(self, properties): - """ Create several properties on the instance at once. - - This is a more efficient version of - :meth:`pyramid.util.InstancePropertyMixin.set_property` which - can accept multiple ``(name, property)`` pairs generated via - :meth:`pyramid.util.InstancePropertyMixin._make_property`. - - ``properties`` is a sequence of two-tuples *or* a data structure - with an ``.items()`` method which returns a sequence of two-tuples - (presumably a dictionary). It will be used to add several - properties to the instance in a manner that is more efficient - than simply calling ``set_property`` repeatedly. - """ + @classmethod + def apply_properties(cls, target, properties): attrs = dict(properties) - if attrs: - parent = self.__class__ - cls = type(parent.__name__, (parent, object), attrs) + parent = target.__class__ + newcls = type(parent.__name__, (parent, object), attrs) # We assign __provides__, __implemented__ and __providedBy__ below # to prevent a memory leak that results from from the usage of this # instance's eventual use in an adapter lookup. Adapter lookup @@ -106,15 +100,34 @@ class InstancePropertyMixin(object): # attached to it val = getattr(parent, name, _marker) if val is not _marker: - setattr(cls, name, val) - self.__class__ = cls + setattr(newcls, name, val) + target.__class__ = newcls + + @classmethod + def set_property(cls, target, callable, name=None, reify=False): + """A helper method to apply a single property to an instance.""" + prop = cls.make_property(callable, name=name, reify=reify) + cls.apply_properties(target, [prop]) + + def add_property(self, callable, name=None, reify=False): + """Add a new property configuration. + + This should be used in combination with :meth:`.apply` as a + more efficient version of :meth:`.set_property`. + """ + name, fn = self.make_property(callable, name=name, reify=reify) + self.properties[name] = fn - def _set_extensions(self, extensions): - for name, fn in iteritems_(extensions.methods): - method = fn.__get__(self, self.__class__) - setattr(self, name, method) + def apply(self, target): + """ Apply all configured properties to the ``target`` instance.""" + if self.properties: + self.apply_properties(target, self.properties) - self._set_properties(extensions.descriptors) +class InstancePropertyMixin(object): + """ Mixin that will allow an instance to add properties at + run-time as if they had been defined via @property or @reify + on the class itself. + """ def set_property(self, callable, name=None, reify=False): """ Add a callable or a property descriptor to the instance. @@ -168,8 +181,8 @@ class InstancePropertyMixin(object): >>> foo.y # notice y keeps the original value 1 """ - prop = self._make_property(callable, name=name, reify=reify) - self._set_properties([prop]) + InstancePropertyHelper.set_property( + self, callable, name=name, reify=reify) class WeakOrderedSet(object): """ Maintain a set of items. -- cgit v1.2.3 From 46bc7fd9e221a084ca2f4d0cb8b158d2e239c373 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 16 Feb 2015 22:14:24 -0600 Subject: update changelog for #1581 --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 37803b3ed..8cee9c09d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,12 @@ Next release Features -------- +- Add ``pyramid.request.apply_request_extensions`` function which can be + used in testing to apply any request extensions configured via + ``config.add_request_method``. Previously it was only possible to test + the extensions by going through Pyramid's router. + See https://github.com/Pylons/pyramid/pull/1581 + - pcreate when run without a scaffold argument will now print information on the missing flag, as well as a list of available scaffolds. See https://github.com/Pylons/pyramid/pull/1566 and -- cgit v1.2.3 From 2f0ba093f1bd50fd43e0a55f244b90d1fe50ff19 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 16 Feb 2015 23:02:43 -0600 Subject: docstring on apply_properties --- pyramid/util.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyramid/util.py b/pyramid/util.py index 63d113361..5721a93fc 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -78,6 +78,9 @@ class InstancePropertyHelper(object): @classmethod def apply_properties(cls, target, properties): + """Accept a list or dict of ``properties`` generated from + :meth:`.make_property` and apply them to a ``target`` object. + """ attrs = dict(properties) if attrs: parent = target.__class__ -- cgit v1.2.3 From 780889f18d17b86fc12625166a245c7f9947cbe6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 01:05:04 -0600 Subject: remove the token from the ICacheBuster api This exposes the QueryStringCacheBuster and PathSegmentCacheBuster public APIs alongside the md5-variants. These should be more cleanly subclassed by people wishing to extend their implementations. --- docs/api/static.rst | 6 +++ docs/narr/assets.rst | 15 ++++---- pyramid/config/views.py | 4 +- pyramid/interfaces.py | 13 ++----- pyramid/static.py | 65 +++++++++++++++++++++++++-------- pyramid/tests/test_config/test_views.py | 12 +++--- pyramid/tests/test_static.py | 16 ++++---- 7 files changed, 82 insertions(+), 49 deletions(-) diff --git a/docs/api/static.rst b/docs/api/static.rst index 543e526ad..b6b279139 100644 --- a/docs/api/static.rst +++ b/docs/api/static.rst @@ -9,6 +9,12 @@ :members: :inherited-members: + .. autoclass:: PathSegmentCacheBuster + :members: + + .. autoclass:: QueryStringCacheBuster + :members: + .. autoclass:: PathSegmentMd5CacheBuster :members: diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index fc908c2b4..d6bc8cbb8 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -446,19 +446,20 @@ In order to implement your own cache buster, you can write your own class from scratch which implements the :class:`~pyramid.interfaces.ICacheBuster` interface. Alternatively you may choose to subclass one of the existing implementations. One of the most likely scenarios is you'd want to change the -way the asset token is generated. To do this just subclass an existing -implementation and replace the :meth:`~pyramid.interfaces.ICacheBuster.token` -method. Here is an example which just uses Git to get the hash of the -currently checked out code: +way the asset token is generated. To do this just subclass either +:class:`~pyramid.static.PathSegmentCacheBuster` or +:class:`~pyramid.static.QueryStringCacheBuster` and define a +``tokenize(pathspec)`` method. Here is an example which just uses Git to get +the hash of the currently checked out code: .. code-block:: python :linenos: import os import subprocess - from pyramid.static import PathSegmentMd5CacheBuster + from pyramid.static import PathSegmentCacheBuster - class GitCacheBuster(PathSegmentMd5CacheBuster): + class GitCacheBuster(PathSegmentCacheBuster): """ Assuming your code is installed as a Git checkout, as opposed to as an egg from an egg repository like PYPI, you can use this cachebuster to @@ -470,7 +471,7 @@ currently checked out code: ['git', 'rev-parse', 'HEAD'], cwd=here).strip() - def token(self, pathspec): + def tokenize(self, pathspec): return self.sha1 Choosing a Cache Buster diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 85e252f2f..24c592f7a 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1980,9 +1980,9 @@ class StaticURLInfo(object): cb = self._default_cachebust() if cb: def cachebust(subpath, kw): - token = cb.token(spec + subpath) subpath_tuple = tuple(subpath.split('/')) - subpath_tuple, kw = cb.pregenerate(token, subpath_tuple, kw) + subpath_tuple, kw = cb.pregenerate( + spec + subpath, subpath_tuple, kw) return '/'.join(subpath_tuple), kw else: cachebust = None diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index d7422bdde..1508f282e 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1192,18 +1192,11 @@ class ICacheBuster(Interface): .. versionadded:: 1.6 """ - def token(pathspec): - """ - Computes and returns a token string used for cache busting. - ``pathspec`` is the path specification for the resource to be cache - busted. """ - - def pregenerate(token, subpath, kw): + def pregenerate(pathspec, subpath, kw): """ Modifies a subpath and/or keyword arguments from which a static asset - URL will be computed during URL generation. The ``token`` argument is - a token string computed by - :meth:`~pyramid.interfaces.ICacheBuster.token` for a particular asset. + URL will be computed during URL generation. The ``pathspec`` argument + is the path specification for the resource to be cache busted. The ``subpath`` argument is a tuple of path elements that represent the portion of the asset URL which is used to find the asset. The ``kw`` argument is a dict of keywords that are to be passed eventually to diff --git a/pyramid/static.py b/pyramid/static.py index c4a9e3cc4..460639a89 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -174,7 +174,7 @@ class Md5AssetTokenGenerator(object): def __init__(self): self.token_cache = {} - def token(self, pathspec): + def tokenize(self, pathspec): # An astute observer will notice that this use of token_cache doesn't # look particularly thread safe. Basic read/write operations on Python # dicts, however, are atomic, so simply accessing and writing values @@ -192,38 +192,55 @@ class Md5AssetTokenGenerator(object): self.token_cache[pathspec] = token = _generate_md5(pathspec) return token -class PathSegmentMd5CacheBuster(Md5AssetTokenGenerator): +class PathSegmentCacheBuster(object): """ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which - inserts an md5 checksum token for cache busting in the path portion of an - asset URL. Generated md5 checksums are cached in order to speed up - subsequent calls. + inserts a token for cache busting in the path portion of an asset URL. + + To use this class, subclass it and provide a ``tokenize`` method which + accepts a ``pathspec`` and returns a token. .. versionadded:: 1.6 """ - def pregenerate(self, token, subpath, kw): + def pregenerate(self, pathspec, subpath, kw): + token = self.tokenize(pathspec) return (token,) + subpath, kw def match(self, subpath): return subpath[1:] -class QueryStringMd5CacheBuster(Md5AssetTokenGenerator): +class PathSegmentMd5CacheBuster(PathSegmentCacheBuster, + Md5AssetTokenGenerator): + """ + An implementation of :class:`~pyramid.interfaces.ICacheBuster` which + inserts an md5 checksum token for cache busting in the path portion of an + asset URL. Generated md5 checksums are cached in order to speed up + subsequent calls. + + .. versionadded:: 1.6 + """ + def __init__(self): + PathSegmentCacheBuster.__init__(self) + Md5AssetTokenGenerator.__init__(self) + +class QueryStringCacheBuster(object): """ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds - an md5 checksum token for cache busting in the query string of an asset - URL. Generated md5 checksums are cached in order to speed up subsequent - calls. + a token for cache busting in the query string of an asset URL. The optional ``param`` argument determines the name of the parameter added to the query string and defaults to ``'x'``. + To use this class, subclass it and provide a ``tokenize`` method which + accepts a ``pathspec`` and returns a token. + .. versionadded:: 1.6 """ def __init__(self, param='x'): - super(QueryStringMd5CacheBuster, self).__init__() self.param = param - def pregenerate(self, token, subpath, kw): + def pregenerate(self, pathspec, subpath, kw): + token = self.tokenize(pathspec) query = kw.setdefault('_query', {}) if isinstance(query, dict): query[self.param] = token @@ -231,7 +248,24 @@ class QueryStringMd5CacheBuster(Md5AssetTokenGenerator): kw['_query'] = tuple(query) + ((self.param, token),) return subpath, kw -class QueryStringConstantCacheBuster(QueryStringMd5CacheBuster): +class QueryStringMd5CacheBuster(QueryStringCacheBuster, + Md5AssetTokenGenerator): + """ + An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds + an md5 checksum token for cache busting in the query string of an asset + URL. Generated md5 checksums are cached in order to speed up subsequent + calls. + + The optional ``param`` argument determines the name of the parameter added + to the query string and defaults to ``'x'``. + + .. versionadded:: 1.6 + """ + def __init__(self, param='x'): + QueryStringCacheBuster.__init__(self, param=param) + Md5AssetTokenGenerator.__init__(self) + +class QueryStringConstantCacheBuster(QueryStringCacheBuster): """ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds an arbitrary token for cache busting in the query string of an asset URL. @@ -245,9 +279,8 @@ class QueryStringConstantCacheBuster(QueryStringMd5CacheBuster): .. versionadded:: 1.6 """ def __init__(self, token, param='x'): + QueryStringCacheBuster.__init__(self, param=param) self._token = token - self.param = param - def token(self, pathspec): + def tokenize(self, pathspec): return self._token - diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index d1eb1ed3c..36c86f78c 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3995,7 +3995,7 @@ class TestStaticURLInfo(unittest.TestCase): def test_add_cachebust_default(self): config = self._makeConfig() inst = self._makeOne() - inst._default_cachebust = DummyCacheBuster + inst._default_cachebust = lambda: DummyCacheBuster('foo') inst.add(config, 'view', 'mypackage:path', cachebust=True) cachebust = config.registry._static_url_registrations[0][3] subpath, kw = cachebust('some/path', {}) @@ -4014,7 +4014,7 @@ class TestStaticURLInfo(unittest.TestCase): config = self._makeConfig() inst = self._makeOne() inst.add(config, 'view', 'mypackage:path', - cachebust=DummyCacheBuster()) + cachebust=DummyCacheBuster('foo')) cachebust = config.registry._static_url_registrations[0][3] subpath, kw = cachebust('some/path', {}) self.assertEqual(subpath, 'some/path') @@ -4127,10 +4127,10 @@ class DummyMultiView: """ """ class DummyCacheBuster(object): - def token(self, pathspec): - return 'foo' - def pregenerate(self, token, subpath, kw): - kw['x'] = token + def __init__(self, token): + self.token = token + def pregenerate(self, pathspec, subpath, kw): + kw['x'] = self.token return subpath, kw def parse_httpdate(s): diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 2f4de249e..a3df74b44 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -393,13 +393,13 @@ class TestMd5AssetTokenGenerator(unittest.TestCase): return cls() def test_package_resource(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize expected = '76d653a3a044e2f4b38bb001d283e3d9' token = fut('pyramid.tests:fixtures/static/index.html') self.assertEqual(token, expected) def test_filesystem_resource(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize expected = 'd5155f250bef0e9923e894dbc713c5dd' with open(self.fspath, 'w') as f: f.write("Are we rich yet?") @@ -407,7 +407,7 @@ class TestMd5AssetTokenGenerator(unittest.TestCase): self.assertEqual(token, expected) def test_cache(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize expected = 'd5155f250bef0e9923e894dbc713c5dd' with open(self.fspath, 'w') as f: f.write("Are we rich yet?") @@ -425,11 +425,11 @@ class TestPathSegmentMd5CacheBuster(unittest.TestCase): def _makeOne(self): from pyramid.static import PathSegmentMd5CacheBuster as cls inst = cls() - inst.token = lambda pathspec: 'foo' + inst.tokenize = lambda pathspec: 'foo' return inst def test_token(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize self.assertEqual(fut('whatever'), 'foo') def test_pregenerate(self): @@ -448,11 +448,11 @@ class TestQueryStringMd5CacheBuster(unittest.TestCase): inst = cls(param) else: inst = cls() - inst.token = lambda pathspec: 'foo' + inst.tokenize = lambda pathspec: 'foo' return inst def test_token(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize self.assertEqual(fut('whatever'), 'foo') def test_pregenerate(self): @@ -490,7 +490,7 @@ class TestQueryStringConstantCacheBuster(TestQueryStringMd5CacheBuster): return inst def test_token(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize self.assertEqual(fut('whatever'), 'foo') def test_pregenerate(self): -- cgit v1.2.3 From 4a9c13647b93c79ba3414c32c96906bc43e325d3 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 02:40:07 -0600 Subject: use super with mixins... for reasons --- pyramid/static.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyramid/static.py b/pyramid/static.py index 460639a89..4ff02f798 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -220,8 +220,7 @@ class PathSegmentMd5CacheBuster(PathSegmentCacheBuster, .. versionadded:: 1.6 """ def __init__(self): - PathSegmentCacheBuster.__init__(self) - Md5AssetTokenGenerator.__init__(self) + super(PathSegmentMd5CacheBuster, self).__init__() class QueryStringCacheBuster(object): """ @@ -262,8 +261,7 @@ class QueryStringMd5CacheBuster(QueryStringCacheBuster, .. versionadded:: 1.6 """ def __init__(self, param='x'): - QueryStringCacheBuster.__init__(self, param=param) - Md5AssetTokenGenerator.__init__(self) + super(QueryStringMd5CacheBuster, self).__init__(param=param) class QueryStringConstantCacheBuster(QueryStringCacheBuster): """ @@ -279,7 +277,7 @@ class QueryStringConstantCacheBuster(QueryStringCacheBuster): .. versionadded:: 1.6 """ def __init__(self, token, param='x'): - QueryStringCacheBuster.__init__(self, param=param) + super(QueryStringConstantCacheBuster, self).__init__(param=param) self._token = token def tokenize(self, pathspec): -- cgit v1.2.3 From 5fdf9a5f63b7731963de7f49df6c29077155525f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 11:39:10 -0600 Subject: update changelog --- CHANGES.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 8cee9c09d..596e5f506 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -20,7 +20,10 @@ Features - Cache busting for static resources has been added and is available via a new argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``. - See https://github.com/Pylons/pyramid/pull/1380 + Core APIs are shipped for both cache busting via query strings and + path segments and may be extended to fit into custom asset pipelines. + See https://github.com/Pylons/pyramid/pull/1380 and + https://github.com/Pylons/pyramid/pull/1583 - Add ``pyramid.config.Configurator.root_package`` attribute and init parameter to assist with includeable packages that wish to resolve -- cgit v1.2.3 From 568a025d3156ee1e7bdf92e14c9eba7390c1dd26 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 18:58:53 -0600 Subject: expose public config phases in pyramid.config --- CHANGES.txt | 4 +++- docs/api/config.rst | 5 +++++ docs/narr/extconfig.rst | 17 ++++++++++------- pyramid/config/__init__.py | 23 ++++++++++++++++------- pyramid/interfaces.py | 3 ++- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 1c82e5f27..f2bedbcc9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -7,7 +7,9 @@ Features - The ``pyramid.config.Configurator`` has grown the ability to allow actions to call other actions during a commit-cycle. This enables much more logic to be placed into actions, such as the ability to invoke other actions - or group them for improved conflict detection. + or group them for improved conflict detection. We have also exposed and + documented the config phases that Pyramid uses in order to further assist + in building conforming addons. See https://github.com/Pylons/pyramid/pull/1513 - Add ``pyramid.request.apply_request_extensions`` function which can be diff --git a/docs/api/config.rst b/docs/api/config.rst index 48dd2f0b9..ae913d32c 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -132,3 +132,8 @@ are being used. .. autoclass:: not_ + +.. attribute:: PHASE0_CONFIG +.. attribute:: PHASE1_CONFIG +.. attribute:: PHASE2_CONFIG +.. attribute:: PHASE3_CONFIG diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index c4d3e0250..c805f1572 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -243,12 +243,17 @@ This means that if an action must be reliably executed before or after another action, the ``order`` must be defined explicitly to make this work. For example, views are dependent on routes being defined. Thus the action created by :meth:`pyramid.config.Configurator.add_route` has an ``order`` of -:const:`pyramid.interfaces.PHASE2_CONFIG`. +:const:`pyramid.config.PHASE2_CONFIG`. Pre-defined Phases ~~~~~~~~~~~~~~~~~~ -:const:`pyramid.interfaces.PHASE1_CONFIG` +:const:`pyramid.config.PHASE0_CONFIG` + +- This phase is reserved for developers who want to execute actions prior + to Pyramid's core directives. + +:const:`pyramid.config.PHASE1_CONFIG` - :meth:`pyramid.config.Configurator.add_renderer` - :meth:`pyramid.config.Configurator.add_route_predicate` @@ -258,12 +263,12 @@ Pre-defined Phases - :meth:`pyramid.config.Configurator.set_default_permission` - :meth:`pyramid.config.Configurator.set_view_mapper` -:const:`pyramid.interfaces.PHASE2_CONFIG` +:const:`pyramid.config.PHASE2_CONFIG` - :meth:`pyramid.config.Configurator.add_route` - :meth:`pyramid.config.Configurator.set_authentication_policy` -``0`` +:const:`pyramid.config.PHASE3_CONFIG` - The default for all builtin or custom directives unless otherwise specified. @@ -285,9 +290,7 @@ but we want it to conflict with any other call to our addon: .. code-block:: python :linenos: - from pyramid.interfaces import PHASE1_CONFIG - - PHASE0_CONFIG = PHASE1_CONFIG - 10 + from pyramid.config import PHASE0_CONFIG def includeme(config): config.add_directive(add_auto_route, 'add_auto_route') diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index b5b5e841d..ea84aa1dc 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -12,7 +12,10 @@ from pyramid.interfaces import ( IDebugLogger, IExceptionResponse, IPredicateList, + PHASE0_CONFIG, PHASE1_CONFIG, + PHASE2_CONFIG, + PHASE3_CONFIG, ) from pyramid.asset import resolve_asset_spec @@ -55,7 +58,9 @@ from pyramid.settings import aslist from pyramid.threadlocal import manager from pyramid.util import ( + ActionInfo, WeakOrderedSet, + action_method, object_description, ) @@ -69,17 +74,18 @@ from pyramid.config.security import SecurityConfiguratorMixin from pyramid.config.settings import SettingsConfiguratorMixin from pyramid.config.testing import TestingConfiguratorMixin from pyramid.config.tweens import TweensConfiguratorMixin -from pyramid.config.util import PredicateList, not_ +from pyramid.config.util import ( + PredicateList, + not_, + PHASE1_CONFIG, + PHASE2_CONFIG, + PHASE3_CONFIG, +) from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin from pyramid.path import DottedNameResolver -from pyramid.util import ( - action_method, - ActionInfo, - ) - empty = text_('') _marker = object() @@ -87,6 +93,10 @@ ConfigurationError = ConfigurationError # pyflakes not_ = not_ # pyflakes, this is an API +PHASE0_CONFIG = PHASE0_CONFIG # api +PHASE1_CONFIG = PHASE1_CONFIG # api +PHASE2_CONFIG = PHASE2_CONFIG # api +PHASE3_CONFIG = PHASE3_CONFIG # api class Configurator( TestingConfiguratorMixin, @@ -1301,4 +1311,3 @@ def expand_action(discriminator, callable=None, args=(), kw=None, ) global_registries = WeakOrderedSet() - diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 1508f282e..4c171f9cc 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1228,6 +1228,7 @@ class ICacheBuster(Interface): # with this phase will be executed earlier than those with later phase # numbers. The default phase number is 0, FTR. +PHASE0_CONFIG = -30 PHASE1_CONFIG = -20 PHASE2_CONFIG = -10 - +PHASE3_CONFIG = 0 -- cgit v1.2.3 From c0063b33e3b570120aab09b7d0a0adcf31c8705c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 19:01:15 -0600 Subject: fix odd sentence --- docs/narr/extconfig.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index c805f1572..47f2fcb46 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -235,7 +235,8 @@ actions. The logic within actions is deferred until a call to :meth:`pyramid.config.Configurator.make_wsgi_app`). This means you may call ``config.add_view(route_name='foo')`` **before** ``config.add_route('foo', '/foo')`` because nothing actually happens until -commit-time when conflicts are resolved, actions are ordered and executed. +commit-time. During a commit cycle conflicts are resolved, actions are ordered +and executed. By default, almost every action in Pyramid has an ``order`` of ``0``. Every action within the same order-level will be executed in the order it was called. -- cgit v1.2.3 From bba15920ee77a626c2ea3636d9d3b4f8d571afa6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 19:02:14 -0600 Subject: avoid saying order=0, instead say PHASE3_CONFIG --- docs/narr/extconfig.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index 47f2fcb46..d17842bf2 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -238,8 +238,9 @@ actions. The logic within actions is deferred until a call to commit-time. During a commit cycle conflicts are resolved, actions are ordered and executed. -By default, almost every action in Pyramid has an ``order`` of ``0``. Every -action within the same order-level will be executed in the order it was called. +By default, almost every action in Pyramid has an ``order`` of +:const:`pyramid.config.PHASE3_CONFIG`. Every action within the same order-level +will be executed in the order it was called. This means that if an action must be reliably executed before or after another action, the ``order`` must be defined explicitly to make this work. For example, views are dependent on routes being defined. Thus the action created -- cgit v1.2.3 From 0bf2fded1a5dfa1614120c989f1d051908fa0b56 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 19:03:13 -0600 Subject: fix syntax --- docs/narr/extconfig.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index d17842bf2..a61eca7b7 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -295,7 +295,7 @@ but we want it to conflict with any other call to our addon: from pyramid.config import PHASE0_CONFIG def includeme(config): - config.add_directive(add_auto_route, 'add_auto_route') + config.add_directive('add_auto_route', add_auto_route) def add_auto_route(config, name, view): def register(): -- cgit v1.2.3 From a8fab3816726affaee2a8b91037372ba77cc1487 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 17 Feb 2015 20:15:11 -0500 Subject: add functest for config reentrancy --- pyramid/tests/test_config/test_init.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 2930734fa..4eb3f3385 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -1554,6 +1554,35 @@ class TestActionState(unittest.TestCase): (3, g, (8,)), ]) +class Test_reentrant_action_functional(unittest.TestCase): + def _makeConfigurator(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + + def test_functional(self): + def add_auto_route(config, name, view): + def register(): + config.add_view(route_name=name, view=view) + config.add_route(name, '/' + name) + config.action( + ('auto route', name), register, order=-30 + ) + config = self._makeConfigurator() + config.add_directive('add_auto_route', add_auto_route) + def my_view(request): + return request.response + config.add_auto_route('foo', my_view) + config.commit() + from pyramid.interfaces import IRoutesMapper + mapper = config.registry.getUtility(IRoutesMapper) + routes = mapper.get_routes() + route = routes[0] + self.assertEqual(len(routes), 1) + self.assertEqual(route.name, 'foo') + self.assertEqual(route.path, '/foo') + + class Test_resolveConflicts(unittest.TestCase): def _callFUT(self, actions): from pyramid.config import resolveConflicts -- cgit v1.2.3 From bae121df8a31fa4303b68d9fcb71283293ad0c79 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 19:22:07 -0600 Subject: dammit, forgot to revert import --- pyramid/config/__init__.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index ea84aa1dc..401def208 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -74,13 +74,7 @@ from pyramid.config.security import SecurityConfiguratorMixin from pyramid.config.settings import SettingsConfiguratorMixin from pyramid.config.testing import TestingConfiguratorMixin from pyramid.config.tweens import TweensConfiguratorMixin -from pyramid.config.util import ( - PredicateList, - not_, - PHASE1_CONFIG, - PHASE2_CONFIG, - PHASE3_CONFIG, -) +from pyramid.config.util import PredicateList, not_ from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin -- cgit v1.2.3 From 4f28c2e2bd59c3fdbfc784d2ba8ef569bbe3b484 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 17 Feb 2015 20:28:38 -0500 Subject: appease coverage --- pyramid/tests/test_config/test_init.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 4eb3f3385..0ed04eb06 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -1570,8 +1570,7 @@ class Test_reentrant_action_functional(unittest.TestCase): ) config = self._makeConfigurator() config.add_directive('add_auto_route', add_auto_route) - def my_view(request): - return request.response + def my_view(request): return request.response config.add_auto_route('foo', my_view) config.commit() from pyramid.interfaces import IRoutesMapper -- cgit v1.2.3 From 750b783e9726684b2860bac4c1ab9d385f4cfb78 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 20:50:33 -0600 Subject: fix typo on changes.rst --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index f2bedbcc9..ca2020cdb 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -148,7 +148,7 @@ Bug Fixes - Allow the ``pyramid.renderers.JSONP`` renderer to work even if there is no valid request object. In this case it will not wrap the object in a - callback and thus behave just like the ``pyramid.renderers.JSON` renderer. + callback and thus behave just like the ``pyramid.renderers.JSON`` renderer. See https://github.com/Pylons/pyramid/pull/1561 - Prevent "parameters to load are deprecated" ``DeprecationWarning`` -- cgit v1.2.3 From 3c163b212a6848c1d45916073d6a60a9020ea5c1 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 21:08:58 -0600 Subject: reword a small part to clarify what's happening with view_config --- docs/narr/urldispatch.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 2fd971917..ca6a55164 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -502,9 +502,10 @@ and an incoming request matches the *pattern* of the route configuration, the :term:`view callable` named as the ``view`` attribute of the route configuration will be invoked. -Recall that ``config.scan`` is equivalent to calling ``config.add_view``, -because the ``@view_config`` decorator in ``mypackage.views``, shown below, -maps the route name to the matching view callable. In the case of the above +Recall that the ``@view_config`` is equivalent to calling ``config.add_view``, +because the ``config.scan()`` call will import ``mypackage.views``, shown +below, and execute ``config.add_view`` under the hood. Each view then maps the +route name to the matching view callable. In the case of the above example, when the URL of a request matches ``/site/{id}``, the view callable at the Python dotted path name ``mypackage.views.site_view`` will be called with the request. In other words, we've associated a view callable directly with a -- cgit v1.2.3 From 459493929a92b14a986ba387bdabd3c551ddee72 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 21:14:49 -0600 Subject: grammar --- docs/narr/security.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 2dc0c76af..a02f65660 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -653,7 +653,7 @@ that implements the following interface: """ After you do so, you can pass an instance of such a class into the -:class:`~pyramid.config.Configurator.set_authentication_policy` method +:class:`~pyramid.config.Configurator.set_authentication_policy` method at configuration time to use it. .. index:: -- cgit v1.2.3 From df966ac2f5c6fc230db920d945be4a6567521e40 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 21:45:56 -0600 Subject: enhance security docs with an example of subclassing a builtin policy --- docs/narr/security.rst | 58 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/docs/narr/security.rst b/docs/narr/security.rst index a02f65660..75f4dc7c5 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -341,9 +341,7 @@ third argument is a permission or sequence of permission names. A principal is usually a user id, however it also may be a group id if your authentication system provides group information and the effective :term:`authentication policy` policy is written to respect group information. -For example, the -:class:`pyramid.authentication.RepozeWho1AuthenticationPolicy` respects group -information if you configure it with a ``callback``. +See :ref:`extending_default_authentication_policies`. Each ACE in an ACL is processed by an authorization policy *in the order dictated by the ACL*. So if you have an ACL like this: @@ -582,6 +580,60 @@ denied or allowed. Introspecting this information in the debugger or via print statements when a call to :meth:`~pyramid.request.Request.has_permission` fails is often useful. +.. index:: + single: authentication policy (extending) + +.. _extending_default_authentication_policies: + +Extending Default Authentication Policies +----------------------------------------- + +Pyramid ships with some builtin authentication policies for use in your +applications. See :mod:`pyramid.authentication` for the available +policies. They differ on their mechanisms for tracking authentication +credentials between requests, however they all interface with your +application in mostly the same way. + +Above you learned about :ref:`assigning_acls`. Each :term:`principal` used +in the :term:`ACL` is matched against the list returned from +:meth:`pyramid.interfaces.IAuthenticationPolicy.effective_principals`. +Similarly, :meth:`pyramid.request.Request.authenticated_userid` maps to +:meth:`pyramid.interfaces.IAuthenticationPolicy.authenticated_userid`. + +You may control these values by subclassing the default authentication +policies. For example, below we subclass the +:class:`pyramid.authentication.AuthTktAuthenticationPolicy` and define +extra functionality to query our database before confirming that the +:term:`userid` is valid in order to avoid blindly trusting the value in the +cookie (what if the cookie is still valid but the user has deleted their +account?). We then use that :term:`userid` to augment the +``effective_principals`` with information about groups and other state for +that user. + +.. code-block:: python + :linenos: + + from pyramid.authentication import AuthTktAuthenticationPolicy + + class MyAuthenticationPolicy(AuthTktAuthenticationPolicy): + def authenticated_userid(self, request): + userid = self.unauthenticated_userid(request) + if userid: + if request.verify_userid_is_still_valid(userid): + return userid + + def effective_principals(self, request): + principals = [Everyone] + userid = self.authenticated_userid(request) + if userid: + principals += [Authenticated, str(userid)] + return principals + +In most instances ``authenticated_userid`` and ``effective_principals`` are +application-specific whereas ``unauthenticated_userid``, ``remember`` and +``forget`` are generic and focused on transport/serialization of data +between consecutive requests. + .. index:: single: authentication policy (creating) -- cgit v1.2.3 From 99bf8b84fbadf5c50232fc90ee2cdc5708b6f6bf Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 22:11:24 -0600 Subject: pserve -b will always open 127.0.0.1 --- pyramid/scripts/pserve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 3b79aabd7..d68075e01 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -350,7 +350,7 @@ class PServeCommand(object): def open_browser(): context = loadcontext(SERVER, app_spec, name=app_name, relative_to=base, global_conf=vars) - url = 'http://{host}:{port}/'.format(**context.config()) + url = 'http://127.0.0.1:{port}/'.format(**context.config()) time.sleep(1) webbrowser.open(url) t = threading.Thread(target=open_browser) -- cgit v1.2.3 From 5ace6591cfb49199befc258ccb256a69c455477e Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Thu, 19 Feb 2015 15:37:43 -0800 Subject: Enhance test_assets to expose #1580 This enhances existing tests so that they detect the issue in #1580. Then I'm going to fix the issue in PR #1587. See #1580 --- pyramid/tests/test_config/test_assets.py | 48 ++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/pyramid/tests/test_config/test_assets.py b/pyramid/tests/test_config/test_assets.py index b605a602d..842c73da6 100644 --- a/pyramid/tests/test_config/test_assets.py +++ b/pyramid/tests/test_config/test_assets.py @@ -54,6 +54,12 @@ class TestAssetsConfiguratorMixin(unittest.TestCase): self.assertEqual(source.package, subpackage) self.assertEqual(source.prefix, 'templates/bar.pt') + resource_name = '' + expected = os.path.join(here, 'pkgs', 'asset', + 'subpackage', 'templates', 'bar.pt') + self.assertEqual(override.source.get_filename(resource_name), + expected) + def test_override_asset_package_with_package(self): from pyramid.config.assets import PackageAssetSource config = self._makeOne(autocommit=True) @@ -71,6 +77,12 @@ class TestAssetsConfiguratorMixin(unittest.TestCase): self.assertEqual(source.package, subpackage) self.assertEqual(source.prefix, '') + resource_name = 'templates/bar.pt' + expected = os.path.join(here, 'pkgs', 'asset', + 'subpackage', 'templates', 'bar.pt') + self.assertEqual(override.source.get_filename(resource_name), + expected) + def test_override_asset_directory_with_directory(self): from pyramid.config.assets import PackageAssetSource config = self._makeOne(autocommit=True) @@ -88,6 +100,12 @@ class TestAssetsConfiguratorMixin(unittest.TestCase): self.assertEqual(source.package, subpackage) self.assertEqual(source.prefix, 'templates/') + resource_name = 'bar.pt' + expected = os.path.join(here, 'pkgs', 'asset', + 'subpackage', 'templates', 'bar.pt') + self.assertEqual(override.source.get_filename(resource_name), + expected) + def test_override_asset_directory_with_package(self): from pyramid.config.assets import PackageAssetSource config = self._makeOne(autocommit=True) @@ -105,6 +123,12 @@ class TestAssetsConfiguratorMixin(unittest.TestCase): self.assertEqual(source.package, subpackage) self.assertEqual(source.prefix, '') + resource_name = 'templates/bar.pt' + expected = os.path.join(here, 'pkgs', 'asset', + 'subpackage', 'templates', 'bar.pt') + self.assertEqual(override.source.get_filename(resource_name), + expected) + def test_override_asset_package_with_directory(self): from pyramid.config.assets import PackageAssetSource config = self._makeOne(autocommit=True) @@ -122,6 +146,12 @@ class TestAssetsConfiguratorMixin(unittest.TestCase): self.assertEqual(source.package, subpackage) self.assertEqual(source.prefix, 'templates/') + resource_name = 'bar.pt' + expected = os.path.join(here, 'pkgs', 'asset', + 'subpackage', 'templates', 'bar.pt') + self.assertEqual(override.source.get_filename(resource_name), + expected) + def test_override_asset_directory_with_absfile(self): from pyramid.exceptions import ConfigurationError config = self._makeOne() @@ -161,6 +191,12 @@ class TestAssetsConfiguratorMixin(unittest.TestCase): self.assertTrue(isinstance(source, FSAssetSource)) self.assertEqual(source.prefix, abspath) + resource_name = '' + expected = os.path.join(here, 'pkgs', 'asset', + 'subpackage', 'templates', 'bar.pt') + self.assertEqual(override.source.get_filename(resource_name), + expected) + def test_override_asset_directory_with_absdirectory(self): from pyramid.config.assets import FSAssetSource config = self._makeOne(autocommit=True) @@ -177,6 +213,12 @@ class TestAssetsConfiguratorMixin(unittest.TestCase): self.assertTrue(isinstance(source, FSAssetSource)) self.assertEqual(source.prefix, abspath) + resource_name = 'bar.pt' + expected = os.path.join(here, 'pkgs', 'asset', + 'subpackage', 'templates', 'bar.pt') + self.assertEqual(override.source.get_filename(resource_name), + expected) + def test_override_asset_package_with_absdirectory(self): from pyramid.config.assets import FSAssetSource config = self._makeOne(autocommit=True) @@ -193,6 +235,12 @@ class TestAssetsConfiguratorMixin(unittest.TestCase): self.assertTrue(isinstance(source, FSAssetSource)) self.assertEqual(source.prefix, abspath) + resource_name = 'bar.pt' + expected = os.path.join(here, 'pkgs', 'asset', + 'subpackage', 'templates', 'bar.pt') + self.assertEqual(override.source.get_filename(resource_name), + expected) + def test__override_not_yet_registered(self): from pyramid.interfaces import IPackageOverrides package = DummyPackage('package') -- cgit v1.2.3 From e51295bee250a144adee0d31b4c6d0a62ad27770 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Thu, 19 Feb 2015 13:57:37 -0800 Subject: Fix asset override with package `AssetsConfiguratorMixin.override_asset` does: ```python __import__(override_package) to_package = sys.modules[override_package] override_source = PackageAssetSource(to_package, override_prefix) ``` so it's assuming that the `package` argument to `PackageAssetSource.__init__` takes a module object. But then `PackageAssetSource` had a bunch of methods that did stuff like: - `pkg_resources.resource_exists(self.package, path)` - `pkg_resources.resource_filename(self.package, path)` - `pkg_resources.resource_stream(self.package, path)` and all these `pkg_resources` functions need their `package_or_requirement` argument to be a **string**; not a module - see https://pythonhosted.org/setuptools/pkg_resources.html#basic-resource-access, which says: > the `package_or_requirement argument` may be either a Python package/module > name (e.g. `foo.bar`) or a `Requirement` instance. This causes errors when overriding assets -- e.g.: I am using Kotti and Kotti has this code (https://github.com/Kotti/Kotti/blob/master/kotti/__init__.py#L251): ```python for override in [a.strip() for a in settings['kotti.asset_overrides'].split() if a.strip()]: config.override_asset(to_override='kotti', override_with=override) ``` A Kotti add-on called kotti_navigation does this (https://github.com/Kotti/kotti_navigation/blob/master/kotti_navigation/__init__.py#L12): ```python settings['kotti.asset_overrides'] += ' kotti_navigation:kotti-overrides/' ``` The above code is all legit as far as I can tell and it works fine in pyramid 1.5.2, but it fails with pyramid master with the following: ```pytb File "/Users/marca/python/virtualenvs/kotti_inventorysvc/lib/python2.7/site-packages/pkg_resources.py", line 959, in resource_filename self, resource_name File "/Users/marca/dev/git-repos/pyramid/pyramid/config/assets.py", line 31, in get_resource_filename filename = overrides.get_filename(resource_name) File "/Users/marca/dev/git-repos/pyramid/pyramid/config/assets.py", line 125, in get_filename result = source.get_filename(path) File "/Users/marca/dev/git-repos/pyramid/pyramid/config/assets.py", line 224, in get_filename if pkg_resources.resource_exists(self.package, path): File "/Users/marca/python/virtualenvs/kotti_inventorysvc/lib/python2.7/site-packages/pkg_resources.py", line 948, in resource_exists return get_provider(package_or_requirement).has_resource(resource_name) File "/Users/marca/python/virtualenvs/kotti_inventorysvc/lib/python2.7/site-packages/pkg_resources.py", line 225, in get_provider __import__(moduleOrReq) TypeError: __import__() argument 1 must be string, not module ``` This was a little tricky to resolve because the `override_asset` function wants to pass a module object to `PackageAssetSource.__init__`, but there are a number of tests in `pyramid/tests/test_config/test_assets.py` that assume that it takes a string. So I ended up making it legal to pass either one, so that I don't have to change as much calling code. See https://github.com/Kotti/kotti_navigation/issues/13 --- pyramid/config/assets.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/pyramid/config/assets.py b/pyramid/config/assets.py index 9da092f08..6dabea358 100644 --- a/pyramid/config/assets.py +++ b/pyramid/config/assets.py @@ -214,6 +214,10 @@ class PackageAssetSource(object): """ def __init__(self, package, prefix): self.package = package + if hasattr(package, '__name__'): + self.pkg_name = package.__name__ + else: + self.pkg_name = package self.prefix = prefix def get_path(self, resource_name): @@ -221,33 +225,33 @@ class PackageAssetSource(object): def get_filename(self, resource_name): path = self.get_path(resource_name) - if pkg_resources.resource_exists(self.package, path): - return pkg_resources.resource_filename(self.package, path) + if pkg_resources.resource_exists(self.pkg_name, path): + return pkg_resources.resource_filename(self.pkg_name, path) def get_stream(self, resource_name): path = self.get_path(resource_name) - if pkg_resources.resource_exists(self.package, path): - return pkg_resources.resource_stream(self.package, path) + if pkg_resources.resource_exists(self.pkg_name, path): + return pkg_resources.resource_stream(self.pkg_name, path) def get_string(self, resource_name): path = self.get_path(resource_name) - if pkg_resources.resource_exists(self.package, path): - return pkg_resources.resource_string(self.package, path) + if pkg_resources.resource_exists(self.pkg_name, path): + return pkg_resources.resource_string(self.pkg_name, path) def exists(self, resource_name): path = self.get_path(resource_name) - if pkg_resources.resource_exists(self.package, path): + if pkg_resources.resource_exists(self.pkg_name, path): return True def isdir(self, resource_name): path = self.get_path(resource_name) - if pkg_resources.resource_exists(self.package, path): - return pkg_resources.resource_isdir(self.package, path) + if pkg_resources.resource_exists(self.pkg_name, path): + return pkg_resources.resource_isdir(self.pkg_name, path) def listdir(self, resource_name): path = self.get_path(resource_name) - if pkg_resources.resource_exists(self.package, path): - return pkg_resources.resource_listdir(self.package, path) + if pkg_resources.resource_exists(self.pkg_name, path): + return pkg_resources.resource_listdir(self.pkg_name, path) class FSAssetSource(object): -- cgit v1.2.3 From a9282da880bfc1da654fc1c547a20fdfb3539fd1 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 20 Feb 2015 17:52:41 -0600 Subject: split out targets for py2-cover and py3-cover and add docs building --- .travis.yml | 4 +++- tox.ini | 70 ++++++++++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index 74b64d343..42b3073c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,9 @@ env: - TOXENV=py34 - TOXENV=pypy - TOXENV=pypy3 - - TOXENV=py26,py32,cover + - TOXENV=py2-docs + - TOXENV=py3-docs + - TOXENV=py2-cover,py3-cover,coverage install: - travis_retry pip install tox diff --git a/tox.ini b/tox.ini index dae31ddd6..22f21a6a3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,27 +1,63 @@ [tox] -skipsdist = True -envlist = - py26,py27,py32,py33,py34,pypy,pypy3,cover +envlist = + py26,py27,py32,py33,py34,pypy,pypy3, + {py2,py3}-docs + {py2,py3}-cover,coverage [testenv] -commands = - {envbindir}/python setup.py dev - {envbindir}/coverage run --source={toxinidir}/pyramid {envbindir}/nosetests --xunit-file=nosetests-{envname}.xml - {envbindir}/coverage xml -o coverage-{envname}.xml +# Most of these are defaults but if you specify any you can't fall back +# to defaults for others. +basepython = + py26: python2.6 + py27: python2.7 + py32: python3.2 + py33: python3.3 + py34: python3.4 + pypy: pypy + pypy3: pypy3 + py2: python2.7 + py3: python3.4 + +commands = + pip install pyramid[testing] + nosetests --with-xunit --xunit-file=nosetests-{envname}.xml {posargs:} + +[testenv:py2-cover] +commands = + pip install pyramid[testing] + coverage run --source=pyramid {envbindir}/nosetests --with-xunit --xunit-file=nosetests-py2.xml {posargs:} + coverage xml -o coverage-py2.xml +setenv = + COVERAGE_FILE=.coverage.py2 + +[testenv:py3-cover] +commands = + pip install pyramid[testing] + coverage run --source=pyramid {envbindir}/nosetests --with-xunit --xunit-file=nosetests-py3.xml {posargs:} + coverage xml -o coverage-py3.xml setenv = - COVERAGE_FILE=.coverage.{envname} + COVERAGE_FILE=.coverage.py3 + +[testenv:py2-docs] +whitelist_externals = make +commands = + pip install pyramid[docs] + make -C docs html -[testenv:cover] +[testenv:py3-docs] +whitelist_externals = make +commands = + pip install pyramid[docs] + make -C docs html + +[testenv:coverage] +basepython = python3.4 commands = - {envbindir}/coverage erase - {envbindir}/coverage combine - {envbindir}/coverage xml - {envbindir}/coverage report --show-missing --fail-under=100 + coverage erase + coverage combine + coverage xml + coverage report --show-missing --fail-under=100 deps = coverage setenv = COVERAGE_FILE=.coverage - -# we separate coverage into its own testenv because a) "last run wins" wrt -# cobertura jenkins reporting and b) pypy and jython can't handle any -# combination of versions of coverage and nosexcover that i can find. -- cgit v1.2.3 From d476e6932f67adee5116620b546590ba0c2083de Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 20 Feb 2015 17:57:25 -0600 Subject: dump xunit for py2/py3 targets --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 22f21a6a3..e0f99e7f6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = py26,py27,py32,py33,py34,pypy,pypy3, - {py2,py3}-docs + {py2,py3}-docs, {py2,py3}-cover,coverage [testenv] @@ -25,7 +25,7 @@ commands = [testenv:py2-cover] commands = pip install pyramid[testing] - coverage run --source=pyramid {envbindir}/nosetests --with-xunit --xunit-file=nosetests-py2.xml {posargs:} + coverage run --source=pyramid {envbindir}/nosetests coverage xml -o coverage-py2.xml setenv = COVERAGE_FILE=.coverage.py2 @@ -33,7 +33,7 @@ setenv = [testenv:py3-cover] commands = pip install pyramid[testing] - coverage run --source=pyramid {envbindir}/nosetests --with-xunit --xunit-file=nosetests-py3.xml {posargs:} + coverage run --source=pyramid {envbindir}/nosetests coverage xml -o coverage-py3.xml setenv = COVERAGE_FILE=.coverage.py3 -- cgit v1.2.3 From 5080a3ffcf39ed1ea6ca9b8076eea445f23b80d5 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 22 Feb 2015 12:59:31 -0600 Subject: ignore new .eggs folder created by "setup.py test" deps --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8dca2069c..1e3f68f26 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.egg *.egg-info +.eggs/ *.pyc *$py.class *.pt.py -- cgit v1.2.3 From 5b8d1e69fa6035a32e3167f48b328b29a1568991 Mon Sep 17 00:00:00 2001 From: David Glick Date: Tue, 24 Feb 2015 15:33:16 -0800 Subject: only reset terminal echo flag if stdin is a tty --- pyramid/scripts/pserve.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index d68075e01..57e4ab012 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -716,11 +716,12 @@ def _turn_sigterm_into_systemexit(): # pragma: no cover def ensure_echo_on(): # pragma: no cover if termios: - fd = sys.stdin.fileno() - attr_list = termios.tcgetattr(fd) - if not attr_list[3] & termios.ECHO: - attr_list[3] |= termios.ECHO - termios.tcsetattr(fd, termios.TCSANOW, attr_list) + fd = sys.stdin + if fd.isatty(): + attr_list = termios.tcgetattr(fd) + if not attr_list[3] & termios.ECHO: + attr_list[3] |= termios.ECHO + termios.tcsetattr(fd, termios.TCSANOW, attr_list) def install_reloader(poll_interval=1, extra_files=None): # pragma: no cover """ -- cgit v1.2.3 From 1bcc347b21e6ce317b1fb31610b570b7b9930419 Mon Sep 17 00:00:00 2001 From: David Glick Date: Tue, 24 Feb 2015 15:35:05 -0800 Subject: update changelog --- CHANGES.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 3084bcfe6..9b10c0831 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -121,7 +121,8 @@ Bug Fixes - Work around an issue where ``pserve --reload`` would leave terminal echo disabled if it reloaded during a pdb session. - See https://github.com/Pylons/pyramid/pull/1577 + See https://github.com/Pylons/pyramid/pull/1577, + https://github.com/Pylons/pyramid/pull/1592 - ``pyramid.wsgi.wsgiapp`` and ``pyramid.wsgi.wsgiapp2`` now raise ``ValueError`` when accidentally passed ``None``. -- cgit v1.2.3 From ec46918c86d4b1e82a1555ed488d453c65663549 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 5 Mar 2015 10:11:56 -0600 Subject: add clone to the IRendererInfo interface --- pyramid/interfaces.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 4c171f9cc..bab91b0ee 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -382,6 +382,9 @@ class IRendererInfo(Interface): settings = Attribute('The deployment settings dictionary related ' 'to the current application') + def clone(): + """ Return a shallow copy that does not share any mutable state.""" + class IRendererFactory(Interface): def __call__(info): """ Return an object that implements -- cgit v1.2.3 From b0218c806d684771b00eb93af58c8482376af349 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 13 Mar 2015 02:26:29 -0700 Subject: Update code example Modify `lines` to include closing parens in source and update corresponding `emphasize-lines`. Closes #1606. --- docs/tutorials/wiki/authorization.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 93cd0c18e..6c98b6f3a 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -197,9 +197,9 @@ Add the following import statements to the head of ``tutorial/tutorial/views.py``: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 6-13,15-17 + :lines: 6-17 :linenos: - :emphasize-lines: 3,6-9,11 + :emphasize-lines: 3,6-11 :language: python (Only the highlighted lines, with other necessary modifications, -- cgit v1.2.3 From 59e7ccee7600af7f8c97102a98f675145fbeae8a Mon Sep 17 00:00:00 2001 From: Junya Hayashi Date: Fri, 13 Mar 2015 19:39:31 +0900 Subject: fix comment in forbidden_view_config --- pyramid/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/view.py b/pyramid/view.py index 02ac8849f..f48592e16 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -380,7 +380,7 @@ class forbidden_view_config(object): @forbidden_view_config() def forbidden(request): - return Response('You are not allowed', status='401 Unauthorized') + return Response('You are not allowed', status='403 Forbidden') All arguments passed to this function have the same meaning as :meth:`pyramid.view.view_config` and each predicate argument restricts -- cgit v1.2.3 From 12b6f58956a50a0ad8e6d9971a0248d8f7997122 Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Sun, 15 Mar 2015 15:18:51 -0400 Subject: Allow passing a custom redirect class for appending slashes --- CHANGES.txt | 4 ++++ docs/narr/urldispatch.rst | 4 +++- pyramid/config/views.py | 11 ++++++++++- pyramid/tests/test_config/test_views.py | 26 +++++++++++++++++++++++++- pyramid/view.py | 5 +++-- 5 files changed, 45 insertions(+), 5 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 19d77eb68..a9fe1be3c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -119,6 +119,10 @@ Features explicitly different from ``request.response``. This does not change the API of a renderer. See https://github.com/Pylons/pyramid/pull/1563 +- ``Configurator().add_notfound_view()`` will now accept anything that + implements the ``IResponse`` interface and will use that as the response + class instead of the default ``HTTPFound``. + Bug Fixes --------- diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index ca6a55164..fa3e734fe 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -842,7 +842,9 @@ route. When configured, along with at least one other route in your application, this view will be invoked if the value of ``PATH_INFO`` does not already end in a slash, and if the value of ``PATH_INFO`` *plus* a slash matches any route's pattern. In this case it does an HTTP redirect to the -slash-appended ``PATH_INFO``. +slash-appended ``PATH_INFO``. In addition you may pass anything that implements +:class:`pyramid.interfaces.IResponse` which will then be used in place of the +default class (:class:`pyramid.httpexceptions.HTTPFound`). Let's use an example. If the following routes are configured in your application: diff --git a/pyramid/config/views.py b/pyramid/config/views.py index aba28467d..af2045da9 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1703,7 +1703,11 @@ class ViewsConfiguratorMixin(object): Pyramid will return the result of the view callable provided as ``view``, as normal. + If ``append_slash`` implements IResponse then that will be used as the + response class instead of the default of ``HTTPFound``. + .. versionadded:: 1.3 + .. versionchanged:: 1.6 """ for arg in ('name', 'permission', 'context', 'for_', 'http_cache'): if arg in predicates: @@ -1737,7 +1741,12 @@ class ViewsConfiguratorMixin(object): settings.update(predicates) if append_slash: view = self._derive_view(view, attr=attr, renderer=renderer) - view = AppendSlashNotFoundViewFactory(view) + if IResponse.implementedBy(append_slash): + view = AppendSlashNotFoundViewFactory( + view, redirect_class=append_slash, + ) + else: + view = AppendSlashNotFoundViewFactory(view) settings['view'] = view else: settings['attr'] = attr diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 180050941..c5db121a0 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1941,7 +1941,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): from pyramid.renderers import null_renderer from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.httpexceptions import HTTPNotFound + from pyramid.httpexceptions import HTTPFound, HTTPNotFound config = self._makeOne(autocommit=True) config.add_route('foo', '/foo/') def view(request): return Response('OK') @@ -1954,6 +1954,30 @@ class TestViewsConfigurationMixin(unittest.TestCase): ctx_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) + self.assertTrue(isinstance(result, HTTPFound)) + self.assertEqual(result.location, '/scriptname/foo/?a=1&b=2') + + def test_add_notfound_view_append_slash_custom_response(self): + from pyramid.response import Response + from pyramid.renderers import null_renderer + from zope.interface import implementedBy + from pyramid.interfaces import IRequest + from pyramid.httpexceptions import HTTPMovedPermanently, HTTPNotFound + config = self._makeOne(autocommit=True) + config.add_route('foo', '/foo/') + def view(request): return Response('OK') + config.add_notfound_view( + view, renderer=null_renderer,append_slash=HTTPMovedPermanently + ) + request = self._makeRequest(config) + request.environ['PATH_INFO'] = '/foo' + request.query_string = 'a=1&b=2' + request.path = '/scriptname/foo' + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPNotFound), + request_iface=IRequest) + result = view(None, request) + self.assertTrue(isinstance(result, HTTPMovedPermanently)) self.assertEqual(result.location, '/scriptname/foo/?a=1&b=2') def test_add_notfound_view_with_view_defaults(self): diff --git a/pyramid/view.py b/pyramid/view.py index f48592e16..a84dde83f 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -252,10 +252,11 @@ class AppendSlashNotFoundViewFactory(object): .. deprecated:: 1.3 """ - def __init__(self, notfound_view=None): + def __init__(self, notfound_view=None, redirect_class=HTTPFound): if notfound_view is None: notfound_view = default_exceptionresponse_view self.notfound_view = notfound_view + self.redirect_class = redirect_class def __call__(self, context, request): path = decode_path_info(request.environ['PATH_INFO'] or '/') @@ -268,7 +269,7 @@ class AppendSlashNotFoundViewFactory(object): qs = request.query_string if qs: qs = '?' + qs - return HTTPFound(location=request.path+'/'+qs) + return self.redirect_class(location=request.path+'/'+qs) return self.notfound_view(context, request) append_slash_notfound_view = AppendSlashNotFoundViewFactory() -- cgit v1.2.3 From 0b527bab1b7745695475b894fdafed9480748b16 Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Sun, 15 Mar 2015 15:37:45 -0400 Subject: Add myself to CONTRIBUTORS.txt --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 4f9bd6e41..3d574f99d 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -244,3 +244,5 @@ Contributors - Geoffrey T. Dairiki, 2015/02/06 - David Glick, 2015/02/12 + +- Donald Stufft, 2015/03/15 -- cgit v1.2.3 From 24358c9d1d474bb81ce423d270041464bc731ce9 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 15 Mar 2015 16:04:43 -0400 Subject: add docs to notfound_view_config decorator code, expand docs to inlcude an example --- pyramid/config/views.py | 20 +++++++++++++++++--- pyramid/view.py | 25 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index af2045da9..6be81163f 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1703,11 +1703,25 @@ class ViewsConfiguratorMixin(object): Pyramid will return the result of the view callable provided as ``view``, as normal. - If ``append_slash`` implements IResponse then that will be used as the - response class instead of the default of ``HTTPFound``. + If the argument provided as ``append_slash`` is not a boolean but + instead implements :class:`~pyramid.interfaces.IResponse`, the + append_slash logic will behave as if ``append_slash=True`` was passed, + but the provided class will be used as the response class instead of + the default :class:`~pyramid.httpexceptions.HTTPFound` response class + when a redirect is performed. For example: + + .. code-block:: python + + from pyramid.httpexceptions import HTTPMovedPermanently + config.add_notfound_view(append_slash=HTTPMovedPermanently) + + The above means that a redirect to a slash-appended route will be + attempted, but instead of :class:`~pyramid.httpexceptions.HTTPFound` + being used, :class:`~pyramid.httpexceptions.HTTPMovedPermanently will + be used` for the redirect response if a slash-appended route is found. - .. versionadded:: 1.3 .. versionchanged:: 1.6 + .. versionadded:: 1.3 """ for arg in ('name', 'permission', 'context', 'for_', 'http_cache'): if arg in predicates: diff --git a/pyramid/view.py b/pyramid/view.py index a84dde83f..b30383003 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -332,6 +332,31 @@ class notfound_view_config(object): redirect to the URL implied by the route; if it does not, Pyramid will return the result of the view callable provided as ``view``, as normal. + If the argument provided as ``append_slash`` is not a boolean but + instead implements :class:`~pyramid.interfaces.IResponse`, the + append_slash logic will behave as if ``append_slash=True`` was passed, + but the provided class will be used as the response class instead of + the default :class:`~pyramid.httpexceptions.HTTPFound` response class + when a redirect is performed. For example: + + .. code-block:: python + + from pyramid.httpexceptions import ( + HTTPMovedPermanently, + HTTPNotFound + ) + + @notfound_view_config(append_slash=HTTPMovedPermanently) + def aview(request): + return HTTPNotFound('not found') + + The above means that a redirect to a slash-appended route will be + attempted, but instead of :class:`~pyramid.httpexceptions.HTTPFound` + being used, :class:`~pyramid.httpexceptions.HTTPMovedPermanently will + be used` for the redirect response if a slash-appended route is found. + + .. versionchanged:: 1.6 + See :ref:`changing_the_notfound_view` for detailed usage information. """ -- cgit v1.2.3 From a7d77fdecaa120f4e095d271b687e3bb628e8696 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 15 Mar 2015 16:41:29 -0400 Subject: be more specific about the append_slash feature addition --- CHANGES.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index a9fe1be3c..639b9b802 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -119,9 +119,10 @@ Features explicitly different from ``request.response``. This does not change the API of a renderer. See https://github.com/Pylons/pyramid/pull/1563 -- ``Configurator().add_notfound_view()`` will now accept anything that - implements the ``IResponse`` interface and will use that as the response - class instead of the default ``HTTPFound``. +- The ``append_slash`` argument of ```Configurator().add_notfound_view()`` will + now accept anything that implements the ``IResponse`` interface and will use + that as the response class instead of the default ``HTTPFound``. See + https://github.com/Pylons/pyramid/pull/1610 Bug Fixes --------- -- cgit v1.2.3 From 4d4ee6a553dcde3a292ee2439c1fe5524fc66aa7 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 21 Mar 2015 02:51:16 -0700 Subject: update pylons sphinx theme on master branch, too --- docs/_themes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_themes b/docs/_themes index b14bf8c2a..02096489e 160000 --- a/docs/_themes +++ b/docs/_themes @@ -1 +1 @@ -Subproject commit b14bf8c2a0d95ae8e3d38d07ad3721370ae6f3f8 +Subproject commit 02096489e62ec6c4b3c64e2e6ee874fb79c6ba10 -- cgit v1.2.3 From c4fe07f2bd053d41859f2caf5ef93dd4819a60a4 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 25 Mar 2015 21:27:16 -0700 Subject: Add a badge and link for latest documentation to complement master --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index adf7eea5e..6de42ea40 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,11 @@ Pyramid .. image:: https://readthedocs.org/projects/pyramid/badge/?version=master :target: http://docs.pylonsproject.org/projects/pyramid/en/master/ - :alt: Documentation Status + :alt: Master Documentation Status + +.. image:: https://readthedocs.org/projects/pyramid/badge/?version=latest + :target: http://docs.pylonsproject.org/projects/pyramid/en/latest/ + :alt: Latest Documentation Status Pyramid is a small, fast, down-to-earth, open source Python web framework. It makes real-world web application development and -- cgit v1.2.3 From 610b6edef76168e6a499871be10ba9ea5ea6aa6d Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 26 Mar 2015 11:54:30 -0500 Subject: fix out of date match_param docs --- docs/narr/viewconfig.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index a0feef8d7..d5203c6ba 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -325,7 +325,7 @@ configured view. ``match_param`` This param may be either a single string of the format "key=value" or a - dict of key/value pairs. + tuple containing one or more of these strings. This argument ensures that the view will only be called when the :term:`request` has key/value pairs in its :term:`matchdict` that equal @@ -334,8 +334,8 @@ configured view. hand side of the expression (``edit``) for the view to "match" the current request. - If the ``match_param`` is a dict, every key/value pair must match for the - predicate to pass. + If the ``match_param`` is a tuple, every key/value pair must match + for the predicate to pass. If ``match_param`` is not supplied, the view will be invoked without consideration of the keys and values in ``request.matchdict``. -- cgit v1.2.3 From 594c252a58428433905dfc66e43c8d087c8bb51d Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 30 Mar 2015 02:35:15 -0700 Subject: remove italics from internal references --- docs/_themes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_themes b/docs/_themes index 02096489e..382cba80f 160000 --- a/docs/_themes +++ b/docs/_themes @@ -1 +1 @@ -Subproject commit 02096489e62ec6c4b3c64e2e6ee874fb79c6ba10 +Subproject commit 382cba80fbd6a7424818d17ec63ca520e485f108 -- cgit v1.2.3