Repository: ckan/ckan

Issue:
Remove AppDispatcher and Pylons middleware
TODO


#### Approach



#### Story points


Discussion:
While I think of it, `paste.urlparser.StaticURLParser` is one of the middlewares that needs replacing in [pylons_app.py](https://github.com/ckan/ckan/blob/master/ckan/config/middleware/pylons_app.py). Probably this: http://flask.pocoo.org/docs/1.0/api/#flask.send_from_directory (which we already use for webassets)

Resolving patch:
diff --git a/ckan/config/middleware/__init__.py b/ckan/config/middleware/__init__.py
--- a/ckan/config/middleware/__init__.py
+++ b/ckan/config/middleware/__init__.py
@@ -4,44 +4,12 @@
 
 import logging
 
-import six
-from six.moves.urllib.parse import urlparse
-
-from ckan.lib.i18n import get_locales_from_config
 from ckan.config.environment import load_environment
 from ckan.config.middleware.flask_app import make_flask_stack
-from ckan.common import config
-from ckan.views import handle_i18n
 
 
 log = logging.getLogger(__name__)
 
-if six.PY2:
-    import webob
-    from routes import request_config as routes_request_config
-    from ckan.config.middleware.pylons_app import make_pylons_stack
-
-    # This monkey-patches the webob request object because of the way it messes
-    # with the WSGI environ.
-
-    # Start of webob.requests.BaseRequest monkey patch
-    original_charset__set = webob.request.BaseRequest._charset__set
-
-    def custom_charset__set(self, charset):
-        original_charset__set(self, charset)
-        if self.environ.get('CONTENT_TYPE', '').startswith(';'):
-            self.environ['CONTENT_TYPE'] = ''
-
-    webob.request.BaseRequest._charset__set = custom_charset__set
-
-    webob.request.BaseRequest.charset = property(
-        webob.request.BaseRequest._charset__get,
-        custom_charset__set,
-        webob.request.BaseRequest._charset__del,
-        webob.request.BaseRequest._charset__get.__doc__)
-
-    # End of webob.requests.BaseRequest monkey patch
-
 # This is a test Flask request context to be used internally.
 # Do not use it!
 _internal_test_request_context = None
@@ -56,117 +24,10 @@ def make_app(conf):
     load_environment(conf)
 
     flask_app = make_flask_stack(conf)
-    if six.PY2:
-        full_stack = True
-        static_files = True
-        pylons_app = make_pylons_stack(
-            conf, full_stack, static_files)
-
-        app = AskAppDispatcherMiddleware(
-            {'pylons_app': pylons_app,
-             'flask_app': flask_app})
-    else:
-        app = flask_app
 
     # Set this internal test request context with the configured environment so
     # it can be used when calling url_for from tests
     global _internal_test_request_context
     _internal_test_request_context = flask_app._wsgi_app.test_request_context()
 
-    return app
-
-
-class AskAppDispatcherMiddleware(object):
-
-    '''
-    Dispatches incoming requests to either the Flask or Pylons apps depending
-    on the WSGI environ.
-
-    Used to help transition from Pylons to Flask, and should be removed once
-    Pylons has been deprecated and all app requests are handled by Flask.
-
-    Each app should handle a call to 'can_handle_request(environ)', responding
-    with a tuple:
-        (<bool>, <app>, [<origin>])
-    where:
-       `bool` is True if the app can handle the payload url,
-       `app` is the wsgi app returning the answer
-       `origin` is an optional string to determine where in the app the url
-        will be handled, e.g. 'core' or 'extension'.
-
-    Order of precedence if more than one app can handle a url:
-        Flask Extension > Pylons Extension > Flask Core > Pylons Core
-    '''
-
-    def __init__(self, apps=None):
-        # Dict of apps managed by this middleware {<app_name>: <app_obj>, ...}
-        self.apps = apps or {}
-        self.default_locale = config.get('ckan.locale_default', 'en')
-        self.locale_list = get_locales_from_config()
-
-    def ask_around(self, environ):
-        '''Checks with all apps whether they can handle the incoming request
-        '''
-        answers = [
-            app._wsgi_app.can_handle_request(environ)
-            for name, app in six.iteritems(self.apps)
-        ]
-        # Sort answers by app name
-        answers = sorted(answers, key=lambda x: x[1])
-        log.debug('Route support answers for {0} {1}: {2}'.format(
-            environ.get('REQUEST_METHOD'), environ.get('PATH_INFO'),
-            answers))
-
-        return answers
-
-    def __call__(self, environ, start_response):
-        '''Determine which app to call by asking each app if it can handle the
-        url and method defined on the eviron'''
-
-        # Process locale part on the incoming request URL so it doesn't affect
-        # the mapper queries
-        handle_i18n(environ)
-
-        app_name = 'pylons_app'  # currently defaulting to pylons app
-        answers = self.ask_around(environ)
-        available_handlers = []
-        for answer in answers:
-            if len(answer) == 2:
-                can_handle, asked_app = answer
-                origin = 'core'
-            else:
-                can_handle, asked_app, origin = answer
-            if can_handle:
-                available_handlers.append('{0}_{1}'.format(asked_app, origin))
-
-        # Enforce order of precedence:
-        # Flask Extension > Pylons Extension > Flask Core > Pylons Core
-        if available_handlers:
-            if 'flask_app_extension' in available_handlers:
-                app_name = 'flask_app'
-            elif 'pylons_app_extension' in available_handlers:
-                app_name = 'pylons_app'
-            elif 'flask_app_core' in available_handlers:
-                app_name = 'flask_app'
-
-        log.debug('Serving request via {0} app'.format(app_name))
-        environ['ckan.app'] = app_name
-        if app_name == 'flask_app':
-            # This request will be served by Flask, but we still need the
-            # Pylons URL builder (Routes) to work
-            parts = urlparse(
-                config.get('ckan.site_url', 'http://0.0.0.0:5000'))
-            request_config = routes_request_config()
-            request_config.host = str(parts.netloc + parts.path)
-            request_config.protocol = str(parts.scheme)
-            request_config.mapper = config['routes.map']
-
-            return self.apps[app_name](environ, start_response)
-        else:
-            # Although this request will be served by Pylons we still
-            # need an application context in order for the Flask URL
-            # builder to work and to be able to access the Flask config
-            flask_app = self.apps['flask_app']._wsgi_app
-
-            with flask_app.test_request_context():
-                return self.apps[app_name](environ, start_response)
+    return flask_app
diff --git a/ckan/config/middleware/common_middleware.py b/ckan/config/middleware/common_middleware.py
--- a/ckan/config/middleware/common_middleware.py
+++ b/ckan/config/middleware/common_middleware.py
@@ -1,62 +1,14 @@
 # encoding: utf-8
 
-"""Common middleware used by both Flask and Pylons app stacks."""
+"""Additional middleware used by the Flask app stack."""
 import hashlib
-import cgi
 
 import six
 from six.moves.urllib.parse import unquote, urlparse
 
 import sqlalchemy as sa
-from webob.request import FakeCGIBody
 
 from ckan.common import config
-from ckan.lib.i18n import get_locales_from_config
-
-
-class RootPathMiddleware(object):
-    '''
-    Prevents the SCRIPT_NAME server variable conflicting with the ckan.root_url
-    config. The routes package uses the SCRIPT_NAME variable and appends to the
-    path and ckan addes the root url causing a duplication of the root path.
-
-    This is a middleware to ensure that even redirects use this logic.
-    '''
-    def __init__(self, app, config):
-        self.app = app
-
-    def __call__(self, environ, start_response):
-        # Prevents the variable interfering with the root_path logic
-        if 'SCRIPT_NAME' in environ:
-            environ['SCRIPT_NAME'] = ''
-
-        return self.app(environ, start_response)
-
-
-class CloseWSGIInputMiddleware(object):
-    '''
-    webob.request.Request has habit to create FakeCGIBody. This leads(
-    during file upload) to creating temporary files that are not closed.
-    For long lived processes this means that for each upload you will
-    spend the same amount of temporary space as size of uploaded
-    file additionally, until server restart(this will automatically
-    close temporary files).
-
-    This middleware is supposed to close such files after each request.
-    '''
-    def __init__(self, app, config):
-        self.app = app
-
-    def __call__(self, environ, start_response):
-        wsgi_input = environ['wsgi.input']
-        if isinstance(wsgi_input, FakeCGIBody):
-            for _, item in wsgi_input.vars.items():
-                if not isinstance(item, cgi.FieldStorage):
-                    continue
-                fp = getattr(item, 'fp', None)
-                if fp is not None:
-                    fp.close()
-        return self.app(environ, start_response)
 
 
 class TrackingMiddleware(object):
diff --git a/ckan/config/middleware/pylons_app.py b/ckan/config/middleware/pylons_app.py
deleted file mode 100644
--- a/ckan/config/middleware/pylons_app.py
+++ /dev/null
@@ -1,289 +0,0 @@
-# encoding: utf-8
-
-import os
-import re
-
-from pylons.wsgiapp import PylonsApp
-
-from beaker.middleware import CacheMiddleware, SessionMiddleware
-from paste.cascade import Cascade
-from paste.registry import RegistryManager
-from paste.urlparser import StaticURLParser
-from ckan.common import asbool
-from paste.fileapp import _FileIter
-from pylons.middleware import ErrorHandler, StatusCodeRedirect
-from routes.middleware import RoutesMiddleware
-from repoze.who.config import WhoConfig
-from repoze.who.middleware import PluggableAuthenticationMiddleware
-from fanstatic import Fanstatic
-
-from ckan.plugins import PluginImplementations
-from ckan.plugins.interfaces import IMiddleware
-import ckan.lib.uploader as uploader
-from ckan.config.middleware import common_middleware
-from ckan.common import config
-
-import logging
-log = logging.getLogger(__name__)
-
-
-def make_pylons_stack(conf, full_stack=True, static_files=True,
-                      **app_conf):
-    """Create a Pylons WSGI application and return it
-
-    ``conf``
-        The inherited configuration for this application. Normally from
-        the [DEFAULT] section of the Paste ini file.
-
-    ``full_stack``
-        Whether this application provides a full WSGI stack (by default,
-        meaning it handles its own exceptions and errors). Disable
-        full_stack when this application is "managed" by another WSGI
-        middleware.
-
-    ``static_files``
-        Whether this application serves its own static files; disable
-        when another web server is responsible for serving them.
-
-    ``app_conf``
-        The application's local configuration. Normally specified in
-        the [app:<name>] section of the Paste ini file (where <name>
-        defaults to main).
-
-    """
-    # The Pylons WSGI app
-    app = pylons_app = CKANPylonsApp()
-
-    for plugin in PluginImplementations(IMiddleware):
-        app = plugin.make_middleware(app, config)
-
-    app = common_middleware.CloseWSGIInputMiddleware(app, config)
-    app = common_middleware.RootPathMiddleware(app, config)
-    # Routing/Session/Cache Middleware
-    app = RoutesMiddleware(app, config['routes.map'])
-    # we want to be able to retrieve the routes middleware to be able to update
-    # the mapper.  We store it in the pylons config to allow this.
-    config['routes.middleware'] = app
-    app = SessionMiddleware(app, config)
-    app = CacheMiddleware(app, config)
-
-    # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
-    # app = QueueLogMiddleware(app)
-    if asbool(config.get('ckan.use_pylons_response_cleanup_middleware',
-                         True)):
-        app = execute_on_completion(app, config,
-                                    cleanup_pylons_response_string)
-
-    # Fanstatic
-    fanstatic_enable_rollup = asbool(
-        conf.get('fanstatic_enable_rollup', False))
-    if asbool(config.get('debug', False)):
-        fanstatic_config = {
-            'versioning': True,
-            'recompute_hashes': True,
-            'minified': False,
-            'bottom': True,
-            'bundle': False,
-            'rollup': fanstatic_enable_rollup,
-        }
-    else:
-        fanstatic_config = {
-            'versioning': True,
-            'recompute_hashes': False,
-            'minified': True,
-            'bottom': True,
-            'bundle': True,
-            'rollup': fanstatic_enable_rollup,
-        }
-    root_path = config.get('ckan.root_path', None)
-    if root_path:
-        root_path = re.sub('/{{LANG}}', '', root_path)
-        fanstatic_config['base_url'] = root_path
-    app = Fanstatic(app, **fanstatic_config)
-
-    for plugin in PluginImplementations(IMiddleware):
-        try:
-            app = plugin.make_error_log_middleware(app, config)
-        except AttributeError:
-            log.critical('Middleware class {0} is missing the method'
-                         'make_error_log_middleware.'
-                         .format(plugin.__class__.__name__))
-
-    if asbool(full_stack):
-        # Handle Python exceptions
-        app = ErrorHandler(app, conf, **config['pylons.errorware'])
-
-        # Display error documents for 400, 403, 404 status codes (and
-        # 500 when debug is disabled)
-        if asbool(config['debug']):
-            app = StatusCodeRedirect(app, [400, 403, 404])
-        else:
-            app = StatusCodeRedirect(app, [400, 403, 404, 500])
-
-    # Initialize repoze.who
-    who_parser = WhoConfig(conf['here'])
-    who_parser.parse(open(conf['who.config_file']))
-
-    app = PluggableAuthenticationMiddleware(
-        app,
-        who_parser.identifiers,
-        who_parser.authenticators,
-        who_parser.challengers,
-        who_parser.mdproviders,
-        who_parser.request_classifier,
-        who_parser.challenge_decider,
-        logging.getLogger('repoze.who'),
-        logging.WARN,  # ignored
-        who_parser.remote_user_key
-    )
-
-    # Establish the Registry for this application
-    app = RegistryManager(app, streaming=False)
-
-    if asbool(static_files):
-        # Serve static files
-        static_max_age = None if not asbool(
-            config.get('ckan.cache_enabled')) \
-            else int(config.get('ckan.static_max_age', 3600))
-
-        static_app = StaticURLParser(
-            config['pylons.paths']['static_files'],
-            cache_max_age=static_max_age)
-        static_parsers = [static_app, app]
-
-        storage_directory = uploader.get_storage_path()
-        if storage_directory:
-            path = os.path.join(storage_directory, 'storage')
-            try:
-                os.makedirs(path)
-            except OSError as e:
-                # errno 17 is file already exists
-                if e.errno != 17:
-                    raise
-
-            storage_app = StaticURLParser(path, cache_max_age=static_max_age)
-            static_parsers.insert(0, storage_app)
-
-        # Configurable extra static file paths
-        extra_static_parsers = []
-        for public_path in config.get(
-                'extra_public_paths', '').split(','):
-            if public_path.strip():
-                extra_static_parsers.append(
-                    StaticURLParser(public_path.strip(),
-                                    cache_max_age=static_max_age)
-                )
-        app = Cascade(extra_static_parsers + static_parsers)
-
-    # Prevent the host from request to be added to the new header location.
-    app = common_middleware.HostHeaderMiddleware(app)
-    # Tracking
-    if asbool(config.get('ckan.tracking_enabled', 'false')):
-        app = common_middleware.TrackingMiddleware(app, config)
-
-    # Add a reference to the actual Pylons app so it's easier to access
-    app._wsgi_app = pylons_app
-
-    return app
-
-
-class CKANPylonsApp(PylonsApp):
-
-    app_name = 'pylons_app'
-
-    def can_handle_request(self, environ):
-        '''
-        Decides whether it can handle a request with the Pylons app by
-        matching the request environ against the route mapper
-
-        Returns (True, 'pylons_app', origin) if this is the case.
-
-        origin can be either 'core' or 'extension' depending on where
-        the route was defined.
-
-        NOTE: There is currently a catch all route for GET requests to
-        point arbitrary urls to templates with the same name:
-
-            map.connect('/*url', controller='template', action='view')
-
-        This means that this function will match all GET requests. This
-        does not cause issues as the Pylons core routes are the last to
-        take precedence so the current behaviour is kept, but it's worth
-        keeping in mind.
-        '''
-
-        pylons_mapper = config['routes.map']
-        match_route = pylons_mapper.routematch(environ=environ)
-        if match_route:
-            match, route = match_route
-            origin = 'core'
-            if hasattr(route, '_ckan_core') and not route._ckan_core:
-                origin = 'extension'
-            log.debug('Pylons route match: {0} Origin: {1}'.format(
-                match, origin))
-            return (True, self.app_name, origin)
-        else:
-            return (False, self.app_name)
-
-
-class CloseCallbackWrapper(object):
-    def __init__(self, iterable, callback, environ):
-        # pylons.fileapp expects app_iter to have `file` attribute.
-        self.file = iterable
-        self.callback = callback
-        self.environ = environ
-
-    def __iter__(self):
-        """
-        return a generator that passes through items from iterable
-        then calls callback(environ).
-        """
-        try:
-            for item in self.file:
-                yield item
-        except GeneratorExit:
-            if hasattr(self.file, 'close'):
-                self.file.close()
-            raise
-        finally:
-            self.callback(self.environ)
-
-
-class FileIterWrapper(CloseCallbackWrapper, _FileIter):
-    """Same CloseCallbackWrapper, just with _FileIter mixin.
-
-    That will prevent pylons from converting file responses into
-    in-memori lists.
-    """
-    pass
-
-
-def execute_on_completion(application, config, callback):
-    """
-    Call callback(environ) once complete response is sent
-    """
-
-    def inner(environ, start_response):
-        try:
-            result = application(environ, start_response)
-        except Exception:
-            callback(environ)
-            raise
-        # paste.fileapp converts non-file responses into list
-        # In order to avoid interception of OOM Killer
-        # file responses wrapped into generator with
-        # _FileIter in parent tree.
-        klass = CloseCallbackWrapper
-        if isinstance(result, _FileIter):
-            klass = FileIterWrapper
-        return klass(result, callback, environ)
-
-    return inner
-
-
-def cleanup_pylons_response_string(environ):
-    try:
-        msg = 'response cleared by pylons response cleanup middleware'
-        environ['pylons.controller']._py_object.response._body = msg
-    except (KeyError, AttributeError):
-        pass


Test patch:
diff --git a/ckan/tests/config/test_middleware.py b/ckan/tests/config/test_middleware.py
--- a/ckan/tests/config/test_middleware.py
+++ b/ckan/tests/config/test_middleware.py
@@ -1,7 +1,6 @@
 # encoding: utf-8
 
 import flask
-import mock
 import pytest
 import wsgiref
 import six
@@ -12,68 +11,13 @@
 import ckan.plugins as p
 import ckan.tests.factories as factories
 from ckan.common import config, _
-from ckan.config.middleware import AskAppDispatcherMiddleware
 from ckan.config.middleware.flask_app import CKANFlask
-if six.PY2:
-    from ckan.config.middleware.pylons_app import CKANPylonsApp
-else:
-    CKANPylonsApp = object
-
-_test_controller = u"ckan.tests.config.test_middleware:MockPylonsController"
 
 
 class MockRoutingPlugin(p.SingletonPlugin):
 
-    p.implements(p.IRoutes)
     p.implements(p.IBlueprint)
 
-    controller = _test_controller
-
-    def before_map(self, _map):
-
-        _map.connect(
-            u"/from_pylons_extension_before_map",
-            controller=self.controller,
-            action=u"view",
-        )
-
-        _map.connect(
-            u"/from_pylons_extension_before_map_post_only",
-            controller=self.controller,
-            action=u"view",
-            conditions={u"method": u"POST"},
-        )
-        # This one conflicts with an extension Flask route
-        _map.connect(
-            u"/pylons_and_flask", controller=self.controller, action=u"view"
-        )
-
-        # This one conflicts with a core Flask route
-        _map.connect(u"/about", controller=self.controller, action=u"view")
-
-        _map.connect(
-            u"/pylons_route_flask_url_for",
-            controller=self.controller,
-            action=u"test_flask_url_for",
-        )
-        _map.connect(
-            u"/pylons_translated",
-            controller=self.controller,
-            action=u"test_translation",
-        )
-
-        return _map
-
-    def after_map(self, _map):
-
-        _map.connect(
-            u"/from_pylons_extension_after_map",
-            controller=self.controller,
-            action=u"view",
-        )
-
-        return _map
-
     def get_blueprint(self):
         # Create Blueprint for plugin
         blueprint = Blueprint(self.name, self.__module__)
@@ -82,11 +26,6 @@ def get_blueprint(self):
             u"/simple_flask", u"flask_plugin_view", flask_plugin_view
         )
 
-        blueprint.add_url_rule(
-            u"/flask_route_pylons_url_for",
-            u"flask_route_pylons_url_for",
-            flask_plugin_view_url_for,
-        )
         blueprint.add_url_rule(
             u"/flask_translated", u"flask_translated", flask_translated_view
         )
@@ -98,28 +37,10 @@ def flask_plugin_view():
     return u"Hello World, this is served from a Flask extension"
 
 
-def flask_plugin_view_url_for():
-    url = h.url_for(controller=_test_controller, action=u"view")
-    return u"This URL was generated by Pylons: {0}".format(url)
-
-
 def flask_translated_view():
     return _(u"Dataset")
 
 
-if six.PY2:
-    class MockPylonsController(p.toolkit.BaseController):
-        def view(self):
-            return u"Hello World, this is served from a Pylons extension"
-
-        def test_flask_url_for(self):
-            url = h.url_for(u"api.get_api", ver=3)
-            return u"This URL was generated by Flask: {0}".format(url)
-
-        def test_translation(self):
-            return _(u"Groups")
-
-
 @pytest.fixture
 def patched_app(app):
     flask_app = app.flask_app
@@ -127,17 +48,9 @@ def patched_app(app):
     def test_view():
         return u"This was served from Flask"
 
-    # This endpoint is defined both in Flask and in Pylons core
     flask_app.add_url_rule(
         u"/flask_core", view_func=test_view, endpoint=u"flask_core.index"
     )
-
-    # This endpoint is defined both in Flask and a Pylons extension
-    flask_app.add_url_rule(
-        u"/pylons_and_flask",
-        view_func=test_view,
-        endpoint=u"pylons_and_flask.index",
-    )
     return app
 
 
@@ -149,157 +62,6 @@ def test_flask_core_route_is_served(patched_app):
     assert six.ensure_text(res.data) == u"This was served from Flask"
 
 
-@pytest.mark.ckan_config(u"ckan.plugins", u"test_routing_plugin")
-class TestMiddlewareWithRoutingPlugin:
-    @pytest.mark.skipif(six.PY3, reason=u"Do not test AskAppDispatcherMiddleware in Py3")
-    def test_ask_around_pylons_extension_route_get_before_map(
-        self, patched_app
-    ):
-        environ = {
-            u"PATH_INFO": u"/from_pylons_extension_before_map",
-            u"REQUEST_METHOD": u"GET",
-        }
-        wsgiref.util.setup_testing_defaults(environ)
-
-        answers = patched_app.app.ask_around(environ)
-
-        # Even though this route is defined in Pylons, there is catch all route
-        # in Flask for all requests to serve static files with the same name,
-        # so we get two positive answers
-        assert answers == [
-            (True, u"flask_app", u"core"),
-            (True, u"pylons_app", u"extension"),
-        ]
-
-    @pytest.mark.skipif(six.PY3, reason=u"Do not test AskAppDispatcherMiddleware in Py3")
-    def test_ask_around_pylons_extension_route_post(self, patched_app):
-        environ = {
-            u"PATH_INFO": u"/from_pylons_extension_before_map_post_only",
-            u"REQUEST_METHOD": u"POST",
-        }
-        wsgiref.util.setup_testing_defaults(environ)
-
-        answers = patched_app.app.ask_around(environ)
-
-        assert answers == [
-            (False, u"flask_app"),
-            (True, u"pylons_app", u"extension"),
-        ]
-
-    @pytest.mark.skipif(six.PY3, reason=u"Do not test AskAppDispatcherMiddleware in Py3")
-    def test_ask_around_pylons_extension_route_post_using_get(
-        self, patched_app
-    ):
-        environ = {
-            u"PATH_INFO": u"/from_pylons_extension_before_map_post_only",
-            u"REQUEST_METHOD": u"GET",
-        }
-        wsgiref.util.setup_testing_defaults(environ)
-
-        answers = patched_app.app.ask_around(environ)
-
-        # Even though this route is defined in Pylons, there is catch all route
-        # in Flask for all requests to serve static files with the same name,
-        # so we get two positive answers
-        assert answers == [
-            (True, u"flask_app", u"core"),
-            (False, u"pylons_app"),
-        ]
-
-    @pytest.mark.skipif(six.PY3, reason=u"Do not test AskAppDispatcherMiddleware in Py3")
-    def test_ask_around_pylons_extension_route_get_after_map(
-        self, patched_app
-    ):
-        environ = {
-            u"PATH_INFO": u"/from_pylons_extension_after_map",
-            u"REQUEST_METHOD": u"GET",
-        }
-        wsgiref.util.setup_testing_defaults(environ)
-
-        answers = patched_app.app.ask_around(environ)
-
-        # Even though this route is defined in Pylons, there is catch all route
-        # in Flask for all requests to serve static files with the same name,
-        # so we get two positive answers
-        assert answers == [
-            (True, u"flask_app", u"core"),
-            (True, u"pylons_app", u"extension"),
-        ]
-
-    def test_flask_extension_route_is_served_by_flask(self, patched_app):
-        res = patched_app.get(u"/simple_flask")
-        assert res.status_code == 200
-
-    @pytest.mark.skipif(six.PY3, reason=u"Do not test AskAppDispatcherMiddleware in Py3")
-    def test_pylons_extension_route_is_served_by_pylons(self, patched_app):
-        res = patched_app.get(u"/from_pylons_extension_before_map")
-        assert (
-            res.data == u"Hello World, this is served from a Pylons extension"
-        )
-
-    @pytest.mark.usefixtures(u"clean_db", u"with_request_context")
-    def test_user_objects_in_g_normal_user(self, app):
-        """
-        A normal logged in user request will have expected user objects added
-        to request.
-        """
-        username = factories.User()[u"name"]
-        test_user_obj = model.User.by_name(username)
-        app.get(
-            u"/simple_flask",
-            environ_overrides={u"REMOTE_USER": username.encode(u"ascii") if six.PY2 else username},
-        )
-        assert flask.g.user == username
-        assert flask.g.userobj == test_user_obj
-        assert flask.g.author == username
-        assert flask.g.remote_addr == u"Unknown IP Address"
-
-    @pytest.mark.usefixtures(u"clean_db")
-    def test_user_objects_in_g_anon_user(self, app):
-        """
-        An anon user request will have expected user objects added to request.
-        """
-        with app.flask_app.app_context():
-            app.get(u"/simple_flask", environ_overrides={u"REMOTE_USER": str(u"")})
-            assert flask.g.user == u""
-            assert flask.g.userobj is None
-            assert flask.g.author == u"Unknown IP Address"
-            assert flask.g.remote_addr == u"Unknown IP Address"
-
-    @pytest.mark.usefixtures(u"clean_db", u"with_request_context")
-    def test_user_objects_in_g_sysadmin(self, app):
-        """
-        A sysadmin user request will have expected user objects added to
-        request.
-        """
-        user = factories.Sysadmin()
-        test_user_obj = model.User.by_name(user[u"name"])
-        app.get(
-            u"/simple_flask",
-            environ_overrides={u"REMOTE_USER": user[u"name"].encode(u"ascii") if six.PY2 else user[u"name"]},
-        )
-        assert flask.g.user == user[u"name"]
-        assert flask.g.userobj == test_user_obj
-        assert flask.g.author == user[u"name"]
-        assert flask.g.remote_addr == u"Unknown IP Address"
-
-    @pytest.mark.skipif(six.PY3, reason=u"Do not test AskAppDispatcherMiddleware in Py3")
-    @pytest.mark.ckan_config(
-        u"ckan.use_pylons_response_cleanup_middleware", True
-    )
-    def test_pylons_route_with_cleanup_middleware_activated(self, app):
-        u"""Test the home page renders with the middleware activated
-
-        We are just testing the home page renders without any troubles and that
-        the middleware has not done anything strange to the response string"""
-
-        response = app.get(url=u"/pylons_translated")
-
-        assert response.status_code == 200
-        # make sure we haven't overwritten the response too early.
-        assert u"cleanup middleware" not in response.data
-
-
 @pytest.mark.ckan_config(u"SECRET_KEY", u"super_secret_stuff")
 def test_secret_key_is_used_if_present(app):
     assert app.flask_app.config[u"SECRET_KEY"] == u"super_secret_stuff"
@@ -319,77 +81,3 @@ def test_no_beaker_secret_crashes(make_app):
     # RuntimeError instead (thrown on `make_flask_stack`)
     with pytest.raises(RuntimeError):
         make_app()
-
-
-@pytest.mark.skipif(six.PY3, reason=u"Do not test AskAppDispatcherMiddleware in Py3")
-@pytest.mark.parametrize(
-    u"rv,app_base",
-    [
-        ((False, u"flask_app"), CKANFlask),
-        ((True, u"pylons_app", u"core"), CKANPylonsApp),
-    ],
-)
-def test_can_handle_request_with_environ(monkeypatch, app, rv, app_base):
-    ckan_app = app.app
-
-    handler = mock.Mock(return_value=rv)
-    monkeypatch.setattr(app_base, u"can_handle_request", handler)
-
-    environ = {u"PATH_INFO": str(u"/")}
-    wsgiref.util.setup_testing_defaults(environ)
-    start_response = mock.MagicMock()
-    ckan_app(environ, start_response)
-
-    assert handler.called_with(environ)
-
-
-@pytest.mark.skipif(six.PY3, reason=u"Do not test AskAppDispatcherMiddleware in Py3")
-def test_ask_around_is_called(monkeypatch, app):
-    ask = mock.MagicMock()
-    monkeypatch.setattr(AskAppDispatcherMiddleware, u"ask_around", ask)
-    res = app.get(u"/")
-    assert ask.called
-    assert res.status_code == 404
-
-
-@pytest.mark.skipif(six.PY3, reason=u"Do not test AskAppDispatcherMiddleware in Py3")
-def test_ask_around_is_called_with_args(monkeypatch, app):
-    ckan_app = app.app
-
-    environ = {}
-    start_response = mock.MagicMock()
-    wsgiref.util.setup_testing_defaults(environ)
-
-    ask = mock.MagicMock()
-    monkeypatch.setattr(AskAppDispatcherMiddleware, u"ask_around", ask)
-
-    ckan_app(environ, start_response)
-    assert ask.called
-    ask.assert_called_with(environ)
-
-
-@pytest.mark.skipif(six.PY3, reason=u"Do not test AskAppDispatcherMiddleware in Py3")
-def test_ask_around_flask_core_route_get(app):
-    ckan_app = app.app
-
-    environ = {u"PATH_INFO": u"/", u"REQUEST_METHOD": u"GET"}
-    wsgiref.util.setup_testing_defaults(environ)
-
-    answers = ckan_app.ask_around(environ)
-
-    assert answers == [(True, u"flask_app", u"core"), (False, u"pylons_app")]
-
-
-@pytest.mark.skipif(six.PY3, reason=u"Do not test AskAppDispatcherMiddleware in Py3")
-def test_ask_around_flask_core_route_post(app):
-    ckan_app = app.app
-
-    environ = {u"PATH_INFO": u"/group/new", u"REQUEST_METHOD": u"POST"}
-    wsgiref.util.setup_testing_defaults(environ)
-
-    answers = ckan_app.ask_around(environ)
-
-    assert answers == [
-        (True, u"flask_app", u"core"),
-        (False, u"pylons_app"),
-    ]
diff --git a/ckan/tests/lib/test_helpers.py b/ckan/tests/lib/test_helpers.py
--- a/ckan/tests/lib/test_helpers.py
+++ b/ckan/tests/lib/test_helpers.py
@@ -229,39 +229,6 @@ def test_url_for_flask_route_new_syntax_request_context(self, app):
             generated_url = h.url_for("api.get_api", ver=3)
             assert generated_url == url
 
-    @pytest.mark.skipif(six.PY3, reason="Pylons was removed in Py3")
-    @pytest.mark.ckan_config("ckan.plugins", "test_routing_plugin")
-    @pytest.mark.usefixtures("with_plugins")
-    def test_url_for_flask_request_using_pylons_url_for(self, app):
-
-        res = app.get("/flask_route_pylons_url_for")
-
-        assert u"This URL was generated by Pylons" in res.body
-        assert u"/from_pylons_extension_before_map" in res.body
-
-    @pytest.mark.skipif(six.PY3, reason="Pylons was removed in Py3")
-    @pytest.mark.ckan_config("ckan.plugins", "test_routing_plugin")
-    @pytest.mark.usefixtures("with_plugins")
-    def test_url_for_pylons_request_using_flask_url_for(self, app):
-
-        res = app.get("/pylons_route_flask_url_for")
-
-        assert u"This URL was generated by Flask" in res.body
-        assert u"/api/3" in res.body
-
-    @pytest.mark.skipif(six.PY3, reason="Pylons was removed in Py3")
-    @pytest.mark.ckan_config("ckan.plugins", "test_routing_plugin")
-    @pytest.mark.usefixtures("with_plugins")
-    def test_url_for_pylons_request_external(self):
-
-        url = "http://example.com/from_pylons_extension_before_map"
-        generated_url = h.url_for(
-            controller="ckan.tests.config.test_middleware:MockPylonsController",
-            action="view",
-            _external=True,
-        )
-        assert generated_url == url
-
 
 class TestHelpersRenderMarkdown(object):
     @pytest.mark.parametrize(
diff --git a/ckan/tests/lib/test_i18n.py b/ckan/tests/lib/test_i18n.py
--- a/ckan/tests/lib/test_i18n.py
+++ b/ckan/tests/lib/test_i18n.py
@@ -140,7 +140,7 @@ def test_translations_from_extensions(self):
 
 @pytest.mark.ckan_config(u"ckan.plugins", u"test_routing_plugin")
 @pytest.mark.usefixtures(u"with_plugins")
-class TestI18nFlaskAndPylons(object):
+class TestI18nFlask(object):
     def test_translation_works_on_flask_and_pylons(self, app):
         resp = app.get(u"/flask_translated")
         assert six.ensure_text(resp.data) == six.text_type(u"Dataset")
@@ -148,13 +148,6 @@ def test_translation_works_on_flask_and_pylons(self, app):
         resp = app.get(u"/es/flask_translated")
         assert six.ensure_text(resp.data) == six.text_type(u"Conjunto de datos")
 
-        if six.PY2:
-            resp = app.get(u"/pylons_translated")
-            assert six.ensure_text(resp.data) == six.text_type(u"Groups")
-
-            resp = app.get(u"/es/pylons_translated")
-            assert six.ensure_text(resp.data) == six.text_type(u"Grupos")
-
     @pytest.mark.ckan_config(u"ckan.i18n_directory", I18N_DUMMY_DIR)
     def test_config_i18n_directory(self, app):
         resp = app.get(u"/flask_translated")
@@ -162,10 +155,3 @@ def test_config_i18n_directory(self, app):
 
         resp = app.get(u"/es/flask_translated")
         assert six.ensure_text(resp.data) == six.text_type(u"Foo baz 123")
-
-        if six.PY2:
-            resp = app.get(u"/pylons_translated")
-            assert six.ensure_text(resp.data) == six.text_type(u"Groups")
-
-            resp = app.get(u"/es/pylons_translated")
-            assert six.ensure_text(resp.data) == six.text_type(u"Bar Buz 321")
diff --git a/ckan/tests/test_coding_standards.py b/ckan/tests/test_coding_standards.py
--- a/ckan/tests/test_coding_standards.py
+++ b/ckan/tests/test_coding_standards.py
@@ -257,7 +257,6 @@ def find_unprefixed_string_literals(filename):
     u"ckan/config/middleware/__init__.py",
     u"ckan/config/middleware/common_middleware.py",
     u"ckan/config/middleware/flask_app.py",
-    u"ckan/config/middleware/pylons_app.py",
     u"ckan/exceptions.py",
     u"ckan/i18n/check_po_files.py",
     u"ckan/lib/activity_streams.py",