diff --git a/.gitignore b/.gitignore index 942b3caba..adc0289cf 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ env*/ venv/ .cache/ .python-version +pyramid-*/ diff --git a/CHANGES.rst b/CHANGES.rst index 30005c96f..2a68c4390 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -37,6 +37,9 @@ Features Bug Fixes --------- +- Fix ``pyramid.config.routes.RoutesConfiguratorMixin.route_prefix_context`` + to avoid removal of leading ``/`` provided in the prefix. + - Fix issues where permissions may be checked on exception views. This is not supposed to happen in normal circumstances. diff --git a/src/pyramid/config/routes.py b/src/pyramid/config/routes.py index 409f36849..86c9a109d 100644 --- a/src/pyramid/config/routes.py +++ b/src/pyramid/config/routes.py @@ -595,11 +595,12 @@ def route_prefix_context(self, route_prefix): if old_route_prefix is None: old_route_prefix = '' - route_prefix = '{}/{}'.format( - old_route_prefix.rstrip('/'), route_prefix.lstrip('/') - ) + if old_route_prefix.strip('/'): + route_prefix = '{}/{}'.format( + old_route_prefix, route_prefix.strip('/') + ) - route_prefix = route_prefix.strip('/') + route_prefix = route_prefix.rstrip('/') if not route_prefix: route_prefix = None diff --git a/tests/pkgs/include_routeprefix_app/__init__.py b/tests/pkgs/include_routeprefix_app/__init__.py new file mode 100644 index 000000000..0ca0f335a --- /dev/null +++ b/tests/pkgs/include_routeprefix_app/__init__.py @@ -0,0 +1,2 @@ +def includeme(config): + config.include('tests.pkgs.include_routeprefix_app.root.configure') diff --git a/tests/pkgs/include_routeprefix_app/nested.py b/tests/pkgs/include_routeprefix_app/nested.py new file mode 100644 index 000000000..8cb8ac4d5 --- /dev/null +++ b/tests/pkgs/include_routeprefix_app/nested.py @@ -0,0 +1,70 @@ +from pyramid.response import Response + + +def aview(request): + return Response(request.path) + + +def configure(config): + # note: + # In order to reuse similar routes for prefix and non-prefixed includes, + # we must add a prefix to the names as well to avoid conflicts. + current_prefix = config.route_prefix or '' + + # note: + # The following definition is equivalent to doing an 'add.route' + # followed by 'config.add_view' with an empty pattern or only '/' + # (i.e.: exactly like the next block definition). + # However, the resolved path will depend on the parent including this + # configuration. If it contains a 'route_prefix', it will be equal to it. + # Otherwise, this would become the equal to the 'root' path. + name = current_prefix + 'nested_root_named_view' + config.add_view(aview, name=name) + + name = current_prefix + 'nested_route_simple' + config.add_route(name, pattern='nested_route_simple') + config.add_view(aview, route_name=name) + + name = current_prefix + 'nested_route_slash' + config.add_route(name, pattern='/nested_route_slash') + config.add_view(aview, route_name=name) + + config.commit() + + with config.route_prefix_context(route_prefix='nested_ctx_simple'): + current_prefix = config.route_prefix or '' + + # note: + # Since the following pattern is empty and is always within a context, + # it is equivalent to simply doing 'config.add_view(view, name=...)'. + name = current_prefix + 'nested_ctx_simple_view' + config.add_route(name, pattern='') + config.add_view(aview, route_name=name) + + name = current_prefix + 'nested_ctx_route_simple' + config.add_route(name, pattern='nested_ctx_route_simple') + config.add_view(aview, route_name=name) + + name = current_prefix + 'nested_ctx_route_slash' + config.add_route(name, pattern='/nested_ctx_route_slash') + config.add_view(aview, route_name=name) + config.commit() + + with config.route_prefix_context(route_prefix='/nested_ctx_slash'): + current_prefix = config.route_prefix or '' + + # note: + # Since the following pattern is empty and is always within a context, + # it is equivalent to simply doing 'config.add_view(view, name=...)'. + name = current_prefix + 'nested_ctx_slash_view' + config.add_route(name, pattern='/') + config.add_view(aview, route_name=name) + + name = current_prefix + 'nested_ctx_route_simple' + config.add_route(name, pattern='nested_ctx_route_simple') + config.add_view(aview, route_name=name) + + name = current_prefix + 'nested_ctx_route_slash' + config.add_route(name, pattern='/nested_ctx_route_slash') + config.add_view(aview, route_name=name) + config.commit() diff --git a/tests/pkgs/include_routeprefix_app/root.py b/tests/pkgs/include_routeprefix_app/root.py new file mode 100644 index 000000000..956fe9190 --- /dev/null +++ b/tests/pkgs/include_routeprefix_app/root.py @@ -0,0 +1,50 @@ +from pyramid.response import Response + + +def aview(request): + return Response(request.path) + + +def configure(config): + # note: + # Since the tests using this application evaluate various path + # combinations with or without slashes (notably for routes using + # pattern='' or pattern='/' explicitly), automatically redirect to any + # corresponding trailing slash variation to simplify HTTP requests. + # This does not affect how *leading* slashes of 'route_prefix' are + # evaluated, since an HTTP request that doesn't start with a '/' path + # is immediately rejected. + config.add_notfound_view(append_slash=True) + + config.add_route('', pattern='/') + config.add_view(aview, route_name='') + config.add_view(aview, name='named_view') + config.commit() + + with config.route_prefix_context(route_prefix='root_ctx_simple'): + config.add_view(aview, name='root_ctx_simple_named_view') + config.add_route('root_ctx_simple', pattern='') + config.add_view(aview, 'root_ctx_simple') + config.add_route('ctx_simple_view', pattern=config.route_prefix) + config.add_view(aview, route_name='ctx_simple_view') + config.include('tests.pkgs.include_routeprefix_app.nested.configure') + config.commit() + + with config.route_prefix_context(route_prefix='/root_ctx_slash'): + config.add_view(aview, name='root_ctx_slash_named_view') + config.add_route('root_ctx_slash', pattern='/') + config.add_view(aview, 'root_ctx_slash') + config.add_route('ctx_slash_view', pattern=config.route_prefix) + config.add_view(aview, route_name='ctx_slash_view') + config.include('tests.pkgs.include_routeprefix_app.nested.configure') + config.commit() + + config.include('tests.pkgs.include_routeprefix_app.nested.configure') + config.include( + 'tests.pkgs.include_routeprefix_app.nested.configure', + route_prefix='prefix_simple', + ) + config.include( + 'tests.pkgs.include_routeprefix_app.nested.configure', + route_prefix='/prefix_slash', + ) diff --git a/tests/test_config/test_init.py b/tests/test_config/test_init.py index 4ecd081e0..e5748e949 100644 --- a/tests/test_config/test_init.py +++ b/tests/test_config/test_init.py @@ -904,6 +904,30 @@ def dummy_subapp(config): root_config.include(dummy_subapp, route_prefix='root') + def test_include_with_route_prefix_no_trailing_slash(self): + root_config = self._makeOne(autocommit=True) + + def dummy_subapp(config): + self.assertEqual(config.route_prefix, '/root') + + root_config.include(dummy_subapp, route_prefix='/root/') + + def test_include_with_route_prefix_with_leading_slash(self): + root_config = self._makeOne(autocommit=True) + + def dummy_subapp(config): + self.assertEqual(config.route_prefix, 'root') + + root_config.include(dummy_subapp, route_prefix='root/') + + def test_include_with_route_prefix_with_leading_no_trailing_slash(self): + root_config = self._makeOne(autocommit=True) + + def dummy_subapp(config): + self.assertEqual(config.route_prefix, '/root') + + root_config.include(dummy_subapp, route_prefix='/root/') + def test_include_with_nested_route_prefix(self): root_config = self._makeOne(autocommit=True, route_prefix='root') @@ -924,6 +948,26 @@ def dummy_subapp(config): root_config.include(dummy_subapp, route_prefix='nested') + def test_include_with_nested_route_prefix_with_leading_slash(self): + root_config = self._makeOne(autocommit=True, route_prefix='/root') + + def dummy_subapp2(config): + self.assertEqual(config.route_prefix, '/root/nested') + + def dummy_subapp3(config): + self.assertEqual(config.route_prefix, '/root/nested/nested2') + config.include(dummy_subapp4) + + def dummy_subapp4(config): + self.assertEqual(config.route_prefix, '/root/nested/nested2') + + def dummy_subapp(config): + self.assertEqual(config.route_prefix, '/root/nested') + config.include(dummy_subapp2) + config.include(dummy_subapp3, route_prefix='/nested2') + + root_config.include(dummy_subapp, route_prefix='/nested') + def test_include_with_missing_source_file(self): import inspect diff --git a/tests/test_integration.py b/tests/test_integration.py index 7ca11e81e..3b0a20dfe 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -739,6 +739,210 @@ def test_three(self): self.assertTrue(b'three' in res.body) +class IncludeRoutePrefixConfigurationTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + from pyramid.config import Configurator + + config = Configurator() + config.include('tests.pkgs.include_routeprefix_app') + + app = config.make_wsgi_app() + cls.testapp = TestApp(app) + cls.config = config + + cls.evaluated_paths = set() + cls.expected_paths = set( + '/' + route.path.strip('/') + for route in cls.config.get_routes_mapper().get_routes() + ) + + @classmethod + def tearDownClass(cls): + cls.config.end() + + # make sure we didn't forget to validate one combination + assert cls.expected_paths - cls.evaluated_paths == set() + + def eval(self, path): + path = path.rstrip('/') if path != '/' else '/' + self.evaluated_paths.add(path) + res = self.testapp.get(path, status=(200, 307)) # noqa + if res.status_code != 200: + path += '/' + self.evaluated_paths.add(path) + res = self.testapp.get(path, status=200) + self.assertEqual(path.encode(), res.body) + + # --- + # 'root' module included without prefix + # --- + + def test_root(self): + self.eval('/') + + def test_root_named_view(self): + self.eval('/named_view') + + # --- + # 'root' module using context with simple prefix + # --- + + def test_root_ctx_prefix_simple_view(self): + self.eval('/root_ctx_simple/ctx_simple_view') + + def test_root_ctx_prefix_simple_ctx_simple_view(self): + self.eval('/root_ctx_simple/root_ctx_simple') + + def test_root_ctx_prefix_simple_nested_view(self): + self.eval('/root_ctx_simple') + + def test_root_ctx_prefix_simple_nested_route_simple(self): + self.eval('/root_ctx_simple/nested_route_simple') + + def test_root_ctx_prefix_simple_nested_route_slash(self): + self.eval('/root_ctx_simple/nested_route_slash') + + def test_root_ctx_prefix_simple_nested_ctx_simple_view(self): + self.eval('/root_ctx_simple/nested_ctx_simple/') + + def test_root_ctx_prefix_simple_nested_ctx_simple_route_simple(self): + self.eval('/root_ctx_simple/nested_ctx_simple/nested_ctx_route_simple') + + def test_root_ctx_prefix_simple_nested_ctx_simple_route_slash(self): + self.eval('/root_ctx_simple/nested_ctx_simple/nested_ctx_route_slash') + + def test_root_ctx_prefix_simple_nested_ctx_slash_view(self): + self.eval('/root_ctx_simple/nested_ctx_slash/') + + def test_root_ctx_prefix_simple_nested_ctx_slash_route_simple(self): + self.eval('/root_ctx_simple/nested_ctx_slash/nested_ctx_route_simple') + + def test_root_ctx_prefix_simple_nested_ctx_slash_route_slash(self): + self.eval('/root_ctx_simple/nested_ctx_slash/nested_ctx_route_slash') + + # --- + # 'root' module using context with slash prefix + # --- + + def test_root_ctx_prefix_slash_view(self): + self.eval('/root_ctx_slash/ctx_slash_view') + + def test_root_ctx_prefix_slash_ctx_slash_view(self): + self.eval('/root_ctx_slash/root_ctx_slash') + + def test_root_ctx_prefix_slash_nested_view(self): + self.eval('/root_ctx_slash') + + def test_root_ctx_prefix_slash_nested_route_simple(self): + self.eval('/root_ctx_slash/nested_route_simple') + + def test_root_ctx_prefix_slash_nested_route_slash(self): + self.eval('/root_ctx_slash/nested_route_slash') + + def test_root_ctx_prefix_slash_nested_ctx_simple_view(self): + self.eval('/root_ctx_slash/nested_ctx_simple/') + + def test_root_ctx_prefix_slash_nested_ctx_simple_route_simple(self): + self.eval('/root_ctx_slash/nested_ctx_simple/nested_ctx_route_simple') + + def test_root_ctx_prefix_slash_nested_ctx_simple_route_slash(self): + self.eval('/root_ctx_slash/nested_ctx_simple/nested_ctx_route_slash') + + def test_root_ctx_prefix_slash_nested_ctx_slash_view(self): + self.eval('/root_ctx_slash/nested_ctx_slash/') + + def test_root_ctx_prefix_slash_nested_ctx_slash_route_simple(self): + self.eval('/root_ctx_slash/nested_ctx_slash/nested_ctx_route_simple') + + def test_root_ctx_prefix_slash_nested_ctx_slash_route_slash(self): + self.eval('/root_ctx_slash/nested_ctx_slash/nested_ctx_route_slash') + + # --- + # 'root' module that includes 'nested' module without prefix + # --- + + def test_root_no_prefix_nested_route_simple(self): + self.eval('/nested_route_simple') + + def test_root_no_prefix_nested_route_slash(self): + self.eval('/nested_route_slash') + + def test_root_no_prefix_nested_context_simple_view(self): + self.eval('/nested_ctx_simple') + + def test_root_no_prefix_nested_context_slash_view(self): + self.eval('/nested_ctx_slash') + + def test_root_no_prefix_slash_nested_ctx_simple_route_simple(self): + self.eval('/nested_ctx_simple/nested_ctx_route_simple') + + def test_root_no_prefix_slash_nested_ctx_simple_route_slash(self): + self.eval('/nested_ctx_simple/nested_ctx_route_slash') + + def test_root_no_prefix_nested_context_slash_route_simple(self): + self.eval('/nested_ctx_slash/nested_ctx_route_simple') + + def test_root_no_prefix_nested_context_slash_route_slash(self): + self.eval('/nested_ctx_slash/nested_ctx_route_slash') + + # --- + # 'root' module that includes 'nested' module with simple prefix + # --- + + def test_root_simple_prefix_nested_route_simple(self): + self.eval('/prefix_simple/nested_route_simple') + + def test_root_simple_prefix_nested_route_slash(self): + self.eval('/prefix_simple/nested_route_slash') + + def test_root_simple_prefix_nested_context_simple_view(self): + self.eval('/prefix_simple/nested_ctx_simple/') + + def test_root_simple_prefix_nested_context_slash_view(self): + self.eval('/prefix_simple/nested_ctx_slash/') + + def test_root_simple_prefix_nested_context_simple_route_simple(self): + self.eval('/prefix_simple/nested_ctx_simple/nested_ctx_route_simple') + + def test_root_simple_prefix_nested_context_simple_route_slash(self): + self.eval('/prefix_simple/nested_ctx_simple/nested_ctx_route_slash') + + def test_root_simple_prefix_nested_context_slash_route_simple(self): + self.eval('/prefix_simple/nested_ctx_slash/nested_ctx_route_simple') + + def test_root_simple_prefix_nested_context_slash_route_slash(self): + self.eval('/prefix_simple/nested_ctx_slash/nested_ctx_route_slash') + + # --- + # 'root' module that includes 'nested' module with slash prefix + # --- + + def test_root_slash_prefix_nested_route_simple(self): + self.eval('/prefix_slash/nested_route_simple') + + def test_root_slash_prefix_nested_route_slash(self): + self.eval('/prefix_slash/nested_route_slash') + + def test_root_slash_prefix_nested_context_simple_view(self): + self.eval('/prefix_slash/nested_ctx_simple/') + + def test_root_slash_prefix_nested_context_slash_view(self): + self.eval('/prefix_slash/nested_ctx_slash/') + + def test_root_slash_prefix_nested_context_simple_route_simple(self): + self.eval('/prefix_slash/nested_ctx_simple/nested_ctx_route_simple') + + def test_root_slash_prefix_nested_context_simple_route_slash(self): + self.eval('/prefix_slash/nested_ctx_simple/nested_ctx_route_slash') + + def test_root_slash_prefix_nested_context_slash_route_simple(self): + self.eval('/prefix_slash/nested_ctx_slash/nested_ctx_route_simple') + + def test_root_slash_prefix_nested_context_slash_route_slash(self): + self.eval('/prefix_slash/nested_ctx_slash/nested_ctx_route_slash') + + class SelfScanAppTest(unittest.TestCase): def setUp(self): from .test_config.pkgs.selfscan import main