changeset 6569:e1ab82613133

backend: replace Pylons with TurboGears2 Replace the no-longer-supported Pylons application framework by TurboGears2 which is largely compatible/similar to Pylons. Some interesting history is described at: https://en.wikipedia.org/wiki/TurboGears Changes by Dominik Ruf: - fix sql config in test.ini Changes by Thomas De Schampheleire: - set-up of test suite - tests: 'fix' repo archival test failure Between Pylons and TurboGears2, there seems to be a small difference in the headers sent for repository archive files, related to character encoding. It is assumed that this difference is not important, and that the test should just align with reality. - remove need to import helpers/app_globals in lib TurboGears2 by default expects helpers and app_globals to be available in lib. For this reason kallithea/lib/__init__.py was originally changed to include those files. However, this triggered several types of circular import problems. If module A imported something from lib (e.g. lib.annotate), and lib.helpers imported (possibly indirectly) module A, then there was a circular import. Fix this by overruling the relevant method of tg AppConfig, which is also hinted in the TurboGears2 code. Hereby, the include of something from lib does not automatically import helpers, greatly reducing the chances of circular import problems. - make sure HTTP error '400' uses the custom error pages TurboGears2 does not by default handle HTTP status code '400 (Bad Request)' via the custom error page handling, causing a standard non-styled error page. - disable transaction manager Kallithea currently handles its own transactions and does not need the TurboGears2 transaction manager. However, TurboGears2 tries to enable it by default and fails, throwing an error during application initialization. The error itself seemed to be harmless for normal application functioning, but was nevertheless confusing. - add backlash as required dependency: backlash is meant as the WebError replacement in TurboGears2 (originally WebError is part of Pylons). When debug==true, it provides an interactive debugger in the browser. When debug==false, backlash is necessary to show backtraces on the console. - misc fixes
author Alessandro Molina <alessandro.molina@axant.it>
date Sun, 29 Jan 2017 21:08:49 +0100
parents 9f8a1212177e
children 26fb17b4f925
files dev_requirements.txt development.ini kallithea/__init__.py kallithea/config/app_cfg.py kallithea/config/environment.py kallithea/config/middleware.py kallithea/config/routing.py kallithea/controllers/admin/repo_groups.py kallithea/controllers/admin/user_groups.py kallithea/controllers/admin/users.py kallithea/controllers/api/__init__.py kallithea/controllers/error.py kallithea/controllers/root.py kallithea/lib/app_globals.py kallithea/lib/base.py kallithea/lib/paster_commands/common.py kallithea/lib/paster_commands/make_index.py kallithea/lib/paster_commands/make_rcextensions.py kallithea/lib/paster_commands/template.ini.mako kallithea/lib/utils.py kallithea/model/notification.py kallithea/model/repo.py kallithea/tests/base.py kallithea/tests/conftest.py kallithea/tests/functional/test_admin_notifications.py kallithea/tests/functional/test_admin_permissions.py kallithea/tests/functional/test_admin_users.py kallithea/tests/functional/test_login.py kallithea/tests/functional/test_my_account.py kallithea/tests/functional/test_pullrequests.py kallithea/tests/models/test_notifications.py kallithea/tests/other/test_libs.py kallithea/tests/test.ini setup.py
diffstat 34 files changed, 284 insertions(+), 309 deletions(-) [+]
line wrap: on
line diff
--- a/dev_requirements.txt	Sun Jan 15 20:57:47 2017 +0100
+++ b/dev_requirements.txt	Sun Jan 29 21:08:49 2017 +0100
@@ -6,3 +6,4 @@
 pytest-catchlog
 mock
 sphinx
+webtest < 3
--- a/development.ini	Sun Jan 15 20:57:47 2017 +0100
+++ b/development.ini	Sun Jan 29 21:08:49 2017 +0100
@@ -512,7 +512,7 @@
 ################################
 
 [loggers]
-keys = root, routes, kallithea, sqlalchemy, gearbox, beaker, templates, whoosh_indexer
+keys = root, routes, kallithea, sqlalchemy, tg, gearbox, beaker, templates, whoosh_indexer
 
 [handlers]
 keys = console, console_sql
@@ -553,6 +553,12 @@
 qualname = kallithea
 propagate = 1
 
+[logger_tg]
+level = DEBUG
+handlers =
+qualname = tg
+propagate = 1
+
 [logger_gearbox]
 level = DEBUG
 handlers =
--- a/kallithea/__init__.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/__init__.py	Sun Jan 29 21:08:49 2017 +0100
@@ -30,10 +30,6 @@
 import sys
 import platform
 
-# temporary aliasing to allow early introduction of imports like 'from tg import request'
-import pylons
-sys.modules['tg'] = pylons
-
 VERSION = (0, 3, 99)
 BACKENDS = {
     'hg': 'Mercurial repository',
--- a/kallithea/config/app_cfg.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/config/app_cfg.py	Sun Jan 29 21:08:49 2017 +0100
@@ -11,76 +11,112 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+Global configuration file for TurboGears2 specific settings in Kallithea.
 
-import os
-import kallithea
+This file complements the .ini file.
+"""
+
 import platform
-
-import pylons
-import mako.lookup
-import formencode
+import os, sys
 
-import kallithea.lib.app_globals as app_globals
-
-from kallithea.config.routing import make_map
+import tg
+from tg import hooks
+from tg.configuration import AppConfig
+from tg.support.converters import asbool
 
-from kallithea.lib import helpers
+from kallithea.lib.middleware.https_fixup import HttpsFixup
+from kallithea.lib.middleware.simplegit import SimpleGit
+from kallithea.lib.middleware.simplehg import SimpleHg
+from kallithea.config.routing import make_map
 from kallithea.lib.auth import set_available_permissions
-from kallithea.lib.utils import repo2db_mapper, make_ui, set_app_settings, \
-    load_rcextensions, check_git_version, set_vcs_config, set_indexer_config
-from kallithea.lib.utils2 import engine_from_config, str2bool
-from kallithea.model.base import init_model
+from kallithea.lib.db_manage import DbManage
+from kallithea.lib.utils import load_rcextensions, make_ui, set_app_settings, set_vcs_config, \
+    set_indexer_config, check_git_version, repo2db_mapper
+from kallithea.lib.utils2 import str2bool
 from kallithea.model.scm import ScmModel
 
-from routes.middleware import RoutesMiddleware
-from paste.cascade import Cascade
-from paste.registry import RegistryManager
-from paste.urlparser import StaticURLParser
-from paste.deploy.converters import asbool
+import formencode
+import kallithea
+
+
+class KallitheaAppConfig(AppConfig):
+    # Note: AppConfig has a misleading name, as it's not the application
+    # configuration, but the application configurator. The AppConfig values are
+    # used as a template to create the actual configuration, which might
+    # overwrite or extend the one provided by the configurator template.
+
+    # To make it clear, AppConfig creates the config and sets into it the same
+    # values that AppConfig itself has. Then the values from the config file and
+    # gearbox options are loaded and merged into the configuration. Then an
+    # after_init_config(conf) method of AppConfig is called for any change that
+    # might depend on options provided by configuration files.
+
+    def __init__(self):
+        super(KallitheaAppConfig, self).__init__()
+
+        self['package'] = kallithea
+
+        self['prefer_toscawidgets2'] = False
+        self['use_toscawidgets'] = False
+
+        self['renderers'] = []
+
+        # Enable json in expose
+        self['renderers'].append('json')
 
-from pylons.middleware import ErrorHandler, StatusCodeRedirect
-from pylons.wsgiapp import PylonsApp
+        # Configure template rendering
+        self['renderers'].append('mako')
+        self['default_renderer'] = 'mako'
+        self['use_dotted_templatenames'] = False
+
+        # Configure Sessions, store data as JSON to avoid pickle security issues
+        self['session.enabled'] = True
+        self['session.data_serializer'] = 'json'
+
+        # Configure the base SQLALchemy Setup
+        self['use_sqlalchemy'] = True
+        self['model'] = kallithea.model.base
+        self['DBSession'] = kallithea.model.meta.Session
+
+        # Configure App without an authentication backend.
+        self['auth_backend'] = None
 
-from kallithea.lib.middleware.simplehg import SimpleHg
-from kallithea.lib.middleware.simplegit import SimpleGit
-from kallithea.lib.middleware.https_fixup import HttpsFixup
-from kallithea.lib.middleware.sessionmiddleware import SecureSessionMiddleware
-from kallithea.lib.middleware.wrapper import RequestWrapper
+        # Use custom error page for these errors. By default, Turbogears2 does not add
+        # 400 in this list.
+        # Explicitly listing all is considered more robust than appending to defaults,
+        # in light of possible future framework changes.
+        self['errorpage.status_codes'] = [400, 401, 403, 404]
 
-def setup_configuration(config, paths, app_conf, test_env, test_index):
+        # Disable transaction manager -- currently Kallithea takes care of transactions itself
+        self['tm.enabled'] = False
+
+base_config = KallitheaAppConfig()
+
+# TODO still needed as long as we use pylonslib
+sys.modules['pylons'] = tg
+
+def setup_configuration(app):
+    config = app.config
 
     # store some globals into kallithea
     kallithea.CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
     kallithea.CELERY_EAGER = str2bool(config['app_conf'].get('celery.always.eager'))
+    kallithea.CONFIG = config
 
-    config['routes.map'] = make_map(config)
-    config['pylons.app_globals'] = app_globals.Globals(config)
-    config['pylons.h'] = helpers
-    kallithea.CONFIG = config
+    # Provide routes mapper to the RoutedController
+    root_controller = app.find_controller('root')
+    root_controller.mapper = config['routes.map'] = make_map(config)
 
     load_rcextensions(root_path=config['here'])
 
-    # Setup cache object as early as possible
-    pylons.cache._push_object(config['pylons.app_globals'].cache)
-
-    # Create the Mako TemplateLookup, with the default auto-escaping
-    config['pylons.app_globals'].mako_lookup = mako.lookup.TemplateLookup(
-        directories=paths['templates'],
-        strict_undefined=True,
-        module_directory=os.path.join(app_conf['cache_dir'], 'templates'),
-        input_encoding='utf-8', default_filters=['escape'],
-        imports=['from webhelpers.html import escape'])
-
-    # sets the c attribute access when don't existing attribute are accessed
-    config['pylons.strict_tmpl_context'] = True
+    # FIXME move test setup code out of here
     test = os.path.split(config['__file__'])[-1] == 'test.ini'
     if test:
-        if test_env is None:
-            test_env = not int(os.environ.get('KALLITHEA_NO_TMP_PATH', 0))
-        if test_index is None:
-            test_index = not int(os.environ.get('KALLITHEA_WHOOSH_TEST_DISABLE', 0))
+        test_env = not int(os.environ.get('KALLITHEA_NO_TMP_PATH', 0))
+        test_index = not int(os.environ.get('KALLITHEA_WHOOSH_TEST_DISABLE', 0))
         if os.environ.get('TEST_DB'):
-            # swap config if we pass enviroment variable
+            # swap config if we pass environment variable
             config['sqlalchemy.url'] = os.environ.get('TEST_DB')
 
         from kallithea.tests.fixture import create_test_env, create_test_index
@@ -93,11 +129,6 @@
         if test_index:
             create_test_index(TESTS_TMP_PATH, config, True)
 
-    # MULTIPLE DB configs
-    # Setup the SQLAlchemy database engine
-    sa_engine = engine_from_config(config, 'sqlalchemy.')
-    init_model(sa_engine)
-
     set_available_permissions(config)
     repos_path = make_ui('db').configitems('paths')[0][1]
     config['base_path'] = repos_path
@@ -108,78 +139,37 @@
         instance_id = '%s-%s' % (platform.uname()[1], os.getpid())
         kallithea.CONFIG['instance_id'] = instance_id
 
-    # CONFIGURATION OPTIONS HERE (note: all config options will override
-    # any Pylons config options)
+    # update kallithea.CONFIG with the meanwhile changed 'config'
+    kallithea.CONFIG.update(config)
 
-    # store config reference into our module to skip import magic of
-    # pylons
-    kallithea.CONFIG.update(config)
+    # configure vcs and indexer libraries (they are supposed to be independent
+    # as much as possible and thus avoid importing tg.config or
+    # kallithea.CONFIG).
     set_vcs_config(kallithea.CONFIG)
     set_indexer_config(kallithea.CONFIG)
 
-    #check git version
     check_git_version()
 
     if str2bool(config.get('initial_repo_scan', True)):
         repo2db_mapper(ScmModel().repo_scan(repos_path),
                        remove_obsolete=False, install_git_hooks=False)
+
     formencode.api.set_stdtranslation(languages=[config.get('lang')])
 
-    return config
-
-def setup_application(config, global_conf, full_stack, static_files):
+hooks.register('configure_new_app', setup_configuration)
 
-    # The Pylons WSGI app
-    app = PylonsApp(config=config)
-
-    # Routing/Session/Cache Middleware
-    app = RoutesMiddleware(app, config['routes.map'], use_method_override=False)
-    app = SecureSessionMiddleware(app, config)
 
-    # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
-    if asbool(config['pdebug']):
-        from kallithea.lib.profiler import ProfilingMiddleware
-        app = ProfilingMiddleware(app)
-
-    if asbool(full_stack):
-
-        from kallithea.lib.middleware.sentry import Sentry
-        from kallithea.lib.middleware.appenlight import AppEnlight
-        if AppEnlight and asbool(config['app_conf'].get('appenlight')):
-            app = AppEnlight(app, config)
-        elif Sentry:
-            app = Sentry(app, config)
-
-        # Handle Python exceptions
-        app = ErrorHandler(app, global_conf, **config['pylons.errorware'])
+def setup_application(app):
+    config = app.config
 
-        # Display error documents for 401, 403, 404 status codes (and
-        # 500 when debug is disabled)
-        # Note: will buffer the output in memory!
-        if asbool(config['debug']):
-            app = StatusCodeRedirect(app)
-        else:
-            app = StatusCodeRedirect(app, [400, 401, 403, 404, 500])
-
-        # we want our low level middleware to get to the request ASAP. We don't
-        # need any pylons stack middleware in them - especially no StatusCodeRedirect buffering
-        app = SimpleHg(app, config)
-        app = SimpleGit(app, config)
+    # we want our low level middleware to get to the request ASAP. We don't
+    # need any stack middleware in them - especially no StatusCodeRedirect buffering
+    app = SimpleHg(app, config)
+    app = SimpleGit(app, config)
 
-        # Enable https redirects based on HTTP_X_URL_SCHEME set by proxy
-        if any(asbool(config.get(x)) for x in ['https_fixup', 'force_https', 'use_htsts']):
-            app = HttpsFixup(app, config)
-
-        app = RequestWrapper(app, config) # logging
-
-    # Establish the Registry for this application
-    app = RegistryManager(app) # thread / request-local module globals / variables
+    # Enable https redirects based on HTTP_X_URL_SCHEME set by proxy
+    if any(asbool(config.get(x)) for x in ['https_fixup', 'force_https', 'use_htsts']):
+        app = HttpsFixup(app, config)
+    return app
 
-    if asbool(static_files):
-        # Serve static files
-        static_app = StaticURLParser(config['pylons.paths']['static_files'])
-        app = Cascade([static_app, app])
-
-    app.config = config
-
-    return app
+hooks.register('before_config', setup_application)
--- a/kallithea/config/environment.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/config/environment.py	Sun Jan 29 21:08:49 2017 +0100
@@ -11,34 +11,11 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
-"""
-    Pylons environment configuration
-"""
+"""WSGI environment setup for Kallithea."""
 
-import os
-import kallithea
-import pylons
-
-from kallithea.config.app_cfg import setup_configuration
+from kallithea.config.app_cfg import base_config
 
-def load_environment(global_conf, app_conf,
-                     test_env=None, test_index=None):
-    """
-    Configure the Pylons environment via the ``pylons.config``
-    object
-    """
-    config = pylons.configuration.PylonsConfig()
+__all__ = ['load_environment']
 
-    # Pylons paths
-    root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-    paths = dict(
-        root=root,
-        controllers=os.path.join(root, 'controllers'),
-        static_files=os.path.join(root, 'public'),
-        templates=[os.path.join(root, 'templates')]
-    )
-
-    # Initialize config with the basic options
-    config.init_app(global_conf, app_conf, package='kallithea', paths=paths)
-
-    return setup_configuration(config, paths, app_conf, test_env, test_index)
+# Use base_config to setup the environment loader function
+load_environment = base_config.make_load_environment()
--- a/kallithea/config/middleware.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/config/middleware.py	Sun Jan 29 21:08:49 2017 +0100
@@ -11,33 +11,35 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
-"""
-    Pylons middleware initialization
-"""
+"""WSGI middleware initialization for the Kallithea application."""
 
-from kallithea.config.app_cfg import setup_application
+from kallithea.config.app_cfg import base_config
 from kallithea.config.environment import load_environment
 
-def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
-    """Create a Pylons WSGI application and return it
+__all__ = ['make_app']
+
+# Use base_config to setup the necessary PasteDeploy application factory.
+# make_base_app will wrap the TurboGears2 app with all the middleware it needs.
+make_base_app = base_config.setup_tg_wsgi_app(load_environment)
 
-    ``global_conf``
-        The inherited configuration for this application. Normally from
-        the [DEFAULT] section of the Paste ini file.
+
+def make_app(global_conf, full_stack=True, **app_conf):
+    """
+    Set up Kallithea with the settings found in the PasteDeploy configuration
+    file used.
 
-    ``full_stack``
-        Whether or not 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.
+    :param global_conf: The global settings for Kallithea (those
+        defined under the ``[DEFAULT]`` section).
+    :type global_conf: dict
+    :param full_stack: Should the whole TurboGears2 stack be set up?
+    :type full_stack: str or bool
+    :return: The Kallithea application with all the relevant middleware
+        loaded.
 
-    ``app_conf``
-        The application's local configuration. Normally specified in
-        the [app:<name>] section of the Paste ini file (where <name>
-        defaults to main).
+    This is the PasteDeploy factory for the Kallithea application.
 
+    ``app_conf`` contains all the application-specific settings (those defined
+    under ``[app:main]``.
     """
-    # Configure the Pylons environment
-    config = load_environment(global_conf, app_conf)
-
-    return setup_application(config, global_conf, full_stack, static_files)
+    app = make_base_app(global_conf, full_stack=full_stack, **app_conf)
+    return app
--- a/kallithea/config/routing.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/config/routing.py	Sun Jan 29 21:08:49 2017 +0100
@@ -28,7 +28,7 @@
 
 def make_map(config):
     """Create, configure and return the routes Mapper"""
-    rmap = Mapper(directory=config['pylons.paths']['controllers'],
+    rmap = Mapper(directory=config['paths']['controllers'],
                   always_scan=config['debug'])
     rmap.minimization = False
     rmap.explicit = False
@@ -46,7 +46,7 @@
         repo_name = match_dict.get('repo_name')
 
         if match_dict.get('f_path'):
-            #fix for multiple initial slashes that causes errors
+            # fix for multiple initial slashes that causes errors
             match_dict['f_path'] = match_dict['f_path'].lstrip('/')
 
         by_id_match = get_repo_by_id(repo_name)
@@ -90,11 +90,6 @@
     def check_int(environ, match_dict):
         return match_dict.get('id').isdigit()
 
-    # The ErrorController route (handles 404/500 error pages); it should
-    # likely stay at the top, ensuring it can always be resolved
-    rmap.connect('/error/{action}', controller='error')
-    rmap.connect('/error/{action}/{id}', controller='error')
-
     #==========================================================================
     # CUSTOM ROUTES HERE
     #==========================================================================
@@ -426,8 +421,8 @@
     #==========================================================================
     # API V2
     #==========================================================================
-    with rmap.submapper(path_prefix=ADMIN_PREFIX,
-                        controller='api/api') as m:
+    with rmap.submapper(path_prefix=ADMIN_PREFIX, controller='api/api',
+                        action='_dispatch') as m:
         m.connect('api', '/api')
 
     #USER JOURNAL
--- a/kallithea/controllers/admin/repo_groups.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/controllers/admin/repo_groups.py	Sun Jan 29 21:08:49 2017 +0100
@@ -32,7 +32,7 @@
 
 from formencode import htmlfill
 
-from tg import request, tmpl_context as c
+from tg import request, tmpl_context as c, app_globals
 from tg.i18n import ugettext as _, ungettext
 from webob.exc import HTTPFound, HTTPForbidden, HTTPNotFound, HTTPInternalServerError
 
@@ -112,7 +112,7 @@
         group_iter = RepoGroupList(_list, perm_level='admin')
         repo_groups_data = []
         total_records = len(group_iter)
-        _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
+        _tmpl_lookup = app_globals.mako_lookup
         template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
 
         repo_group_name = lambda repo_group_name, children_groups: (
--- a/kallithea/controllers/admin/user_groups.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/controllers/admin/user_groups.py	Sun Jan 29 21:08:49 2017 +0100
@@ -30,7 +30,7 @@
 import formencode
 
 from formencode import htmlfill
-from tg import request, tmpl_context as c, config
+from tg import request, tmpl_context as c, config, app_globals
 from tg.i18n import ugettext as _
 from webob.exc import HTTPFound
 
@@ -94,7 +94,7 @@
         group_iter = UserGroupList(_list, perm_level='admin')
         user_groups_data = []
         total_records = len(group_iter)
-        _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
+        _tmpl_lookup = app_globals.mako_lookup
         template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
 
         user_group_name = lambda user_group_id, user_group_name: (
--- a/kallithea/controllers/admin/users.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/controllers/admin/users.py	Sun Jan 29 21:08:49 2017 +0100
@@ -30,7 +30,7 @@
 import formencode
 
 from formencode import htmlfill
-from tg import request, tmpl_context as c, config
+from tg import request, tmpl_context as c, config, app_globals
 from tg.i18n import ugettext as _
 from sqlalchemy.sql.expression import func
 from webob.exc import HTTPFound, HTTPNotFound
@@ -73,7 +73,7 @@
 
         users_data = []
         total_records = len(c.users_list)
-        _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
+        _tmpl_lookup = app_globals.mako_lookup
         template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
 
         grav_tmpl = '<div class="gravatar">%s</div>'
--- a/kallithea/controllers/api/__init__.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/controllers/api/__init__.py	Sun Jan 29 21:08:49 2017 +0100
@@ -32,12 +32,9 @@
 import time
 import itertools
 
-from paste.response import replace_header
-from pylons.controllers import WSGIController
-from pylons.controllers.util import Response
-from tg import request
+from tg import Response, response, request, TGController
 
-from webob.exc import HTTPError
+from webob.exc import HTTPError, HTTPException, WSGIHTTPException
 
 from kallithea.model.db import User
 from kallithea.model import meta
@@ -59,19 +56,20 @@
         return safe_str(self.message)
 
 
-class JSONRPCErrorResponse(Response, Exception):
+class JSONRPCErrorResponse(Response, HTTPException):
     """
     Generate a Response object with a JSON-RPC error body
     """
 
     def __init__(self, message=None, retid=None, code=None):
+        HTTPException.__init__(self, message, self)
         Response.__init__(self,
-                          body=json.dumps(dict(id=retid, result=None, error=message)),
+                          json_body=dict(id=retid, result=None, error=message),
                           status=code,
                           content_type='application/json')
 
 
-class JSONRPCController(WSGIController):
+class JSONRPCController(TGController):
     """
      A WSGI-speaking JSON-RPC controller class
 
@@ -95,19 +93,15 @@
         """
         return self._rpc_args
 
-    def __call__(self, environ, start_response):
+    def _dispatch(self, state, remainder=None):
         """
         Parse the request body as JSON, look up the method on the
         controller and if it exists, dispatch to it.
         """
-        try:
-            return self._handle_request(environ, start_response)
-        except JSONRPCErrorResponse as e:
-            return e
-        finally:
-            meta.Session.remove()
+        # Since we are here we should respond as JSON
+        response.content_type = 'application/json'
 
-    def _handle_request(self, environ, start_response):
+        environ = state.request.environ
         start = time.time()
         ip_addr = request.ip_addr = self._get_ip_addr(environ)
         self._req_id = None
@@ -218,39 +212,26 @@
                 )
 
         self._rpc_args = {}
-
         self._rpc_args.update(self._request_params)
-
         self._rpc_args['action'] = self._req_method
         self._rpc_args['environ'] = environ
-        self._rpc_args['start_response'] = start_response
 
-        status = []
-        headers = []
-        exc_info = []
-
-        def change_content(new_status, new_headers, new_exc_info=None):
-            status.append(new_status)
-            headers.extend(new_headers)
-            exc_info.append(new_exc_info)
-
-        output = WSGIController.__call__(self, environ, change_content)
-        output = list(output) # expand iterator - just to ensure exact timing
-        replace_header(headers, 'Content-Type', 'application/json')
-        start_response(status[0], headers, exc_info[0])
         log.info('IP: %s Request to %s time: %.3fs' % (
             self._get_ip_addr(environ),
             safe_unicode(_get_access_path(environ)), time.time() - start)
         )
-        return output
 
-    def _dispatch_call(self):
+        state.set_action(self._rpc_call, [])
+        state.set_params(self._rpc_args)
+        return state
+
+    def _rpc_call(self, action, environ, **rpc_args):
         """
-        Implement dispatch interface specified by WSGIController
+        Call the specified RPC Method
         """
         raw_response = ''
         try:
-            raw_response = self._inspect_call(self._func)
+            raw_response = getattr(self, action)(**rpc_args)
             if isinstance(raw_response, HTTPError):
                 self._error = str(raw_response)
         except JSONRPCError as e:
--- a/kallithea/controllers/error.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/controllers/error.py	Sun Jan 29 21:08:49 2017 +0100
@@ -29,9 +29,8 @@
 import cgi
 import logging
 
-from tg import tmpl_context as c, request, config
+from tg import tmpl_context as c, request, config, expose
 from tg.i18n import ugettext as _
-from pylons.middleware import media_path
 
 from kallithea.lib.base import BaseController, render
 
@@ -52,8 +51,9 @@
         # disable all base actions since we don't need them here
         pass
 
-    def document(self):
-        resp = request.environ.get('pylons.original_response')
+    @expose('/errors/error_document.html')
+    def document(self, *args, **kwargs):
+        resp = request.environ.get('tg.original_response')
         c.site_name = config.get('title')
 
         log.debug('### %s ###', resp and resp.status or 'no response')
@@ -70,7 +70,7 @@
             c.error_message = _('No response')
             c.error_explanation = _('Unknown error')
 
-        return render('/errors/error_document.html')
+        return dict()
 
     def get_error_explanation(self, code):
         """ get the error explanations of int codes
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kallithea/controllers/root.py	Sun Jan 29 21:08:49 2017 +0100
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+from tgext.routes import RoutedController
+from kallithea.lib.base import BaseController
+from kallithea.controllers.error import ErrorController
+
+
+# With TurboGears, the RootController is the controller from which all routing
+# starts from. It is 'magically' found based on the fact that a controller
+# 'foo' is expected to have a class name FooController, located in a file
+# foo.py, inside config['paths']['controllers']. The name 'root' for the root
+# controller is the default name. The dictionary config['paths'] determines the
+# directories where templates, static files and controllers are found. It is
+# set up in tg.AppConfig based on AppConfig['package'] ('kallithea') and the
+# respective defaults 'templates', 'public' and 'controllers'.
+# Inherit from RoutedController to allow Kallithea to use regex-based routing.
+class RootController(RoutedController, BaseController):
+
+    # the following assignment hooks in error handling
+    error = ErrorController()
--- a/kallithea/lib/app_globals.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/lib/app_globals.py	Sun Jan 29 21:08:49 2017 +0100
@@ -25,9 +25,8 @@
 :copyright: (c) 2013 RhodeCode GmbH, and others.
 :license: GPLv3, see LICENSE.md for more details.
 """
-
-from beaker.cache import CacheManager
-from beaker.util import parse_cache_config_options
+import tg
+from tg import config
 
 
 class Globals(object):
@@ -36,11 +35,18 @@
     life of the application
     """
 
-    def __init__(self, config):
+    def __init__(self):
         """One instance of Globals is created during application
         initialization and is available during requests via the
         'app_globals' variable
 
         """
-        self.cache = CacheManager(**parse_cache_config_options(config))
         self.available_permissions = None   # propagated after init_model
+
+    @property
+    def cache(self):
+        return tg.cache
+
+    @property
+    def mako_lookup(self):
+        return config['render_functions']['mako'].normal_loader
--- a/kallithea/lib/base.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/lib/base.py	Sun Jan 29 21:08:49 2017 +0100
@@ -41,9 +41,8 @@
 import paste.httpheaders
 from webhelpers.pylonslib import secure_form
 
-from tg import config, tmpl_context as c, request, response, session
-from pylons.controllers import WSGIController
-from pylons.templating import render_mako as render  # don't remove this import
+from tg import config, tmpl_context as c, request, response, session, render_template
+from tg import TGController
 from tg.i18n import ugettext as _
 
 from kallithea import __version__, BACKENDS
@@ -66,6 +65,10 @@
 log = logging.getLogger(__name__)
 
 
+def render(template_path):
+    return render_template({'url': url}, 'mako', template_path)
+
+
 def _filter_proxy(ip):
     """
     HEADERS can have multiple ips inside the left-most being the original
@@ -101,7 +104,7 @@
 
 def _get_access_path(environ):
     path = environ.get('PATH_INFO')
-    org_req = environ.get('pylons.original_request')
+    org_req = environ.get('tg.original_request')
     if org_req:
         path = org_req.environ.get('PATH_INFO')
     return path
@@ -375,14 +378,11 @@
             meta.Session.remove()
 
 
-class BaseController(WSGIController):
+class BaseController(TGController):
 
     def _before(self, *args, **kwargs):
-        pass
-
-    def __before__(self):
         """
-        __before__ is called before controller methods and after __call__
+        _before is called before controller methods and after __call__
         """
         c.kallithea_version = __version__
         rc_config = Setting.get_app_settings()
@@ -437,13 +437,6 @@
 
         self.scm_model = ScmModel()
 
-        # __before__ in Pylons is called _before in TurboGears2. As preparation
-        # to the migration to TurboGears2, all __before__ methods were already
-        # renamed to _before.  We call them from here to keep the behavior.
-        # This is a temporary call that will be removed in the real TurboGears2
-        # migration commit.
-        self._before()
-
     @staticmethod
     def _determine_auth_user(api_key, bearer_token, session_authuser):
         """
@@ -530,12 +523,7 @@
             log.error('%r request with payload parameters; WebOb should have stopped this', request.method)
             raise webob.exc.HTTPBadRequest()
 
-    def __call__(self, environ, start_response):
-        """Invoke the Controller"""
-
-        # WSGIController.__call__ dispatches to the Controller method
-        # the request is routed to. This routing information is
-        # available in environ['pylons.routes_dict']
+    def __call__(self, environ, context):
         try:
             request.ip_addr = _get_ip_addr(environ)
             # make sure that we update permissions each time we call controller
@@ -564,11 +552,9 @@
                 request.ip_addr, request.authuser,
                 safe_unicode(_get_access_path(environ)),
             )
-            return WSGIController.__call__(self, environ, start_response)
+            return super(BaseController, self).__call__(environ, context)
         except webob.exc.HTTPException as e:
-            return e(environ, start_response)
-        finally:
-            meta.Session.remove()
+            return e
 
 
 class BaseRepoController(BaseController):
--- a/kallithea/lib/paster_commands/common.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/lib/paster_commands/common.py	Sun Jan 29 21:08:49 2017 +0100
@@ -82,12 +82,12 @@
         """
         Read the config file and initialize logging and the application.
         """
-        from tg import config as pylonsconfig
+        from kallithea.config.middleware import make_app
 
         path_to_ini_file = os.path.realpath(config_file)
         conf = paste.deploy.appconfig('config:' + path_to_ini_file)
         logging.config.fileConfig(path_to_ini_file)
-        pylonsconfig.init_app(conf.global_conf, conf.local_conf)
+        make_app(conf.global_conf, **conf.local_conf)
 
     def _init_session(self):
         """
--- a/kallithea/lib/paster_commands/make_index.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/lib/paster_commands/make_index.py	Sun Jan 29 21:08:49 2017 +0100
@@ -40,7 +40,7 @@
     "Kallithea: Create or update full text search index"
 
     def take_action(self, args):
-        from pylons import config
+        from tg import config
         index_location = config['index_dir']
         load_rcextensions(config['here'])
 
--- a/kallithea/lib/paster_commands/make_rcextensions.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/lib/paster_commands/make_rcextensions.py	Sun Jan 29 21:08:49 2017 +0100
@@ -44,7 +44,7 @@
     takes_config_file = False
 
     def take_action(self, args):
-        from pylons import config
+        from tg import config
 
         here = config['here']
         content = pkg_resources.resource_string(
--- a/kallithea/lib/paster_commands/template.ini.mako	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/lib/paster_commands/template.ini.mako	Sun Jan 29 21:08:49 2017 +0100
@@ -516,7 +516,7 @@
 <%text>################################</%text>
 
 [loggers]
-keys = root, routes, kallithea, sqlalchemy, gearbox, beaker, templates, whoosh_indexer
+keys = root, routes, kallithea, sqlalchemy, tg, gearbox, beaker, templates, whoosh_indexer
 
 [handlers]
 keys = console, console_sql
@@ -557,6 +557,12 @@
 qualname = kallithea
 propagate = 1
 
+[logger_tg]
+level = DEBUG
+handlers =
+qualname = tg
+propagate = 1
+
 [logger_gearbox]
 level = DEBUG
 handlers =
--- a/kallithea/lib/utils.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/lib/utils.py	Sun Jan 29 21:08:49 2017 +0100
@@ -32,6 +32,7 @@
 import traceback
 import beaker
 
+from tg import request, response
 from webhelpers.text import collapse, remove_formatting, strip_tags
 from beaker.cache import _cache_decorate
 
--- a/kallithea/model/notification.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/model/notification.py	Sun Jan 29 21:08:49 2017 +0100
@@ -29,7 +29,7 @@
 import logging
 import traceback
 
-from tg import tmpl_context as c
+from tg import tmpl_context as c, app_globals
 from tg.i18n import ugettext as _
 from sqlalchemy.orm import joinedload, subqueryload
 
@@ -274,8 +274,8 @@
 
     def __init__(self):
         super(EmailNotificationModel, self).__init__()
-        self._template_root = kallithea.CONFIG['pylons.paths']['templates'][0]
-        self._tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
+        self._template_root = kallithea.CONFIG['paths']['templates'][0]
+        self._tmpl_lookup = app_globals.mako_lookup
         self.email_types = {
             self.TYPE_CHANGESET_COMMENT: 'changeset_comment',
             self.TYPE_PASSWORD_RESET: 'password_reset',
--- a/kallithea/model/repo.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/model/repo.py	Sun Jan 29 21:08:49 2017 +0100
@@ -153,10 +153,10 @@
     @classmethod
     def _render_datatable(cls, tmpl, *args, **kwargs):
         import kallithea
-        from tg import tmpl_context as c, request
+        from tg import tmpl_context as c, request, app_globals
         from tg.i18n import ugettext as _
 
-        _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
+        _tmpl_lookup = app_globals.mako_lookup
         template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
 
         tmpl = template.get_def(tmpl)
--- a/kallithea/tests/base.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/tests/base.py	Sun Jan 29 21:08:49 2017 +0100
@@ -21,14 +21,9 @@
 import time
 
 from tg import config
-import pylons
-from pylons import url
-from pylons.i18n.translation import _get_translator
-from pylons.util import ContextObj
-from routes.util import URLGenerator
 from webtest import TestApp
 
-from kallithea import is_windows
+from kallithea import is_windows, model
 from kallithea.model.db import Notification, User, UserNotification
 from kallithea.model.meta import Session
 from kallithea.lib.utils2 import safe_str
@@ -42,6 +37,10 @@
 skipif = pytest.mark.skipif
 parametrize = pytest.mark.parametrize
 
+# Hack: These module global values MUST be set to actual values before running any tests. This is currently done by conftest.py.
+url = None
+testapp = None
+
 __all__ = [
     'skipif', 'parametrize', 'environ', 'url', 'TestController',
     'ldap_lib_installed', 'pam_lib_installed', 'invalidate_all_caches',
@@ -147,17 +146,9 @@
 
     @pytest.fixture(autouse=True)
     def app_fixture(self):
-        config = pylons.test.pylonsapp.config
-        url._push_object(URLGenerator(config['routes.map'], environ))
-        pylons.app_globals._push_object(config['pylons.app_globals'])
-        pylons.config._push_object(config)
-        pylons.tmpl_context._push_object(ContextObj())
-        # Initialize a translator for tests that utilize i18n
-        translator = _get_translator(pylons.config.get('lang'))
-        pylons.translator._push_object(translator)
         h = NullHandler()
         logging.getLogger("kallithea").addHandler(h)
-        self.app = TestApp(pylons.test.pylonsapp)
+        self.app = TestApp(testapp)
         return self.app
 
     def remove_all_notifications(self):
--- a/kallithea/tests/conftest.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/tests/conftest.py	Sun Jan 29 21:08:49 2017 +0100
@@ -1,18 +1,20 @@
 import os
 import sys
 import logging
+import pkg_resources
 
-import pkg_resources
 from paste.deploy import loadapp
-import pylons.test
-from pylons.i18n.translation import _get_translator
+from routes.util import URLGenerator
+from tg import config
+
 import pytest
 from kallithea.model.user import UserModel
 from kallithea.model.meta import Session
 from kallithea.model.db import Setting, User, UserIpMap
 from kallithea.tests.base import invalidate_all_caches, TEST_USER_REGULAR_LOGIN
+import kallithea.tests.base # FIXME: needed for setting testapp instance!!!
 
-from kallithea.tests.test_context import test_context
+from tg.util.webtest import test_context
 
 def pytest_configure():
     path = os.getcwd()
@@ -21,14 +23,10 @@
 
     # Disable INFO logging of test database creation, restore with NOTSET
     logging.disable(logging.INFO)
-    pylons.test.pylonsapp = loadapp('config:kallithea/tests/test.ini', relative_to=path)
+    kallithea.tests.base.testapp = loadapp('config:kallithea/tests/test.ini', relative_to=path)
     logging.disable(logging.NOTSET)
 
-    # Initialize a translator for tests that utilize i18n
-    translator = _get_translator(pylons.config.get('lang'))
-    pylons.translator._push_object(translator)
-
-    return pylons.test.pylonsapp
+    kallithea.tests.base.url = URLGenerator(config['routes.map'], kallithea.tests.base.environ)
 
 
 @pytest.fixture
--- a/kallithea/tests/functional/test_admin_notifications.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/tests/functional/test_admin_notifications.py	Sun Jan 29 21:08:49 2017 +0100
@@ -6,7 +6,7 @@
 from kallithea.model.meta import Session
 from kallithea.lib import helpers as h
 
-from kallithea.tests.test_context import test_context
+from tg.util.webtest import test_context
 
 class TestNotificationsController(TestController):
     def setup_method(self, method):
--- a/kallithea/tests/functional/test_admin_permissions.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/tests/functional/test_admin_permissions.py	Sun Jan 29 21:08:49 2017 +0100
@@ -5,7 +5,7 @@
 from kallithea.model.meta import Session
 from kallithea.tests.base import *
 
-from kallithea.tests.test_context import test_context
+from tg.util.webtest import test_context
 
 
 class TestAdminPermissionsController(TestController):
--- a/kallithea/tests/functional/test_admin_users.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/tests/functional/test_admin_users.py	Sun Jan 29 21:08:49 2017 +0100
@@ -17,7 +17,6 @@
 import pytest
 from kallithea.tests.base import *
 from kallithea.tests.fixture import Fixture
-from kallithea.tests.test_context import test_context
 from kallithea.controllers.admin.users import UsersController
 from kallithea.model.db import User, Permission, UserIpMap, UserApiKeys
 from kallithea.lib.auth import check_password
@@ -27,6 +26,8 @@
 from kallithea.model.meta import Session
 from webob.exc import HTTPNotFound
 
+from tg.util.webtest import test_context
+
 fixture = Fixture()
 
 @pytest.fixture
--- a/kallithea/tests/functional/test_login.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/tests/functional/test_login.py	Sun Jan 29 21:08:49 2017 +0100
@@ -16,7 +16,7 @@
 from kallithea.model.meta import Session
 from kallithea.model.user import UserModel
 
-from kallithea.tests.test_context import test_context
+from tg.util.webtest import test_context
 
 fixture = Fixture()
 
--- a/kallithea/tests/functional/test_my_account.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/tests/functional/test_my_account.py	Sun Jan 29 21:08:49 2017 +0100
@@ -7,7 +7,7 @@
 from kallithea.model.user import UserModel
 from kallithea.model.meta import Session
 
-from kallithea.tests.test_context import test_context
+from tg.util.webtest import test_context
 
 fixture = Fixture()
 
--- a/kallithea/tests/functional/test_pullrequests.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/tests/functional/test_pullrequests.py	Sun Jan 29 21:08:49 2017 +0100
@@ -1,7 +1,7 @@
 import re
 import pytest
 
-from kallithea.tests.test_context import test_context
+from tg.util.webtest import test_context
 
 from kallithea.tests.base import *
 from kallithea.tests.fixture import Fixture
--- a/kallithea/tests/models/test_notifications.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/tests/models/test_notifications.py	Sun Jan 29 21:08:49 2017 +0100
@@ -14,7 +14,7 @@
 import kallithea.lib.celerylib
 import kallithea.lib.celerylib.tasks
 
-from kallithea.tests.test_context import test_context
+from tg.util.webtest import test_context
 
 class TestNotifications(TestController):
 
--- a/kallithea/tests/other/test_libs.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/tests/other/test_libs.py	Sun Jan 29 21:08:49 2017 +0100
@@ -31,7 +31,7 @@
 from kallithea.tests.base import *
 from kallithea.lib.utils2 import AttributeDict
 from kallithea.model.db import Repository
-from kallithea.tests.test_context import test_context
+from tg.util.webtest import test_context
 
 proto = 'http'
 TEST_URLS = [
@@ -224,7 +224,7 @@
         from kallithea.lib.helpers import gravatar_url
         _md5 = lambda s: hashlib.md5(s).hexdigest()
 
-        #mock pylons.tmpl_context
+        # mock tg.tmpl_context
         def fake_tmpl_context(_url):
             _c = AttributeDict()
             _c.visual = AttributeDict()
@@ -236,31 +236,31 @@
         fake_url = FakeUrlGenerator(current_url='https://example.com')
         with mock.patch('kallithea.config.routing.url', fake_url):
             fake = fake_tmpl_context(_url='http://example.com/{email}')
-            with mock.patch('pylons.tmpl_context', fake):
+            with mock.patch('tg.tmpl_context', fake):
                     from kallithea.config.routing import url
                     assert url.current() == 'https://example.com'
                     grav = gravatar_url(email_address='test@example.com', size=24)
                     assert grav == 'http://example.com/test@example.com'
 
             fake = fake_tmpl_context(_url='http://example.com/{email}')
-            with mock.patch('pylons.tmpl_context', fake):
+            with mock.patch('tg.tmpl_context', fake):
                 grav = gravatar_url(email_address='test@example.com', size=24)
                 assert grav == 'http://example.com/test@example.com'
 
             fake = fake_tmpl_context(_url='http://example.com/{md5email}')
-            with mock.patch('pylons.tmpl_context', fake):
+            with mock.patch('tg.tmpl_context', fake):
                 em = 'test@example.com'
                 grav = gravatar_url(email_address=em, size=24)
                 assert grav == 'http://example.com/%s' % (_md5(em))
 
             fake = fake_tmpl_context(_url='http://example.com/{md5email}/{size}')
-            with mock.patch('pylons.tmpl_context', fake):
+            with mock.patch('tg.tmpl_context', fake):
                 em = 'test@example.com'
                 grav = gravatar_url(email_address=em, size=24)
                 assert grav == 'http://example.com/%s/%s' % (_md5(em), 24)
 
             fake = fake_tmpl_context(_url='{scheme}://{netloc}/{md5email}/{size}')
-            with mock.patch('pylons.tmpl_context', fake):
+            with mock.patch('tg.tmpl_context', fake):
                 em = 'test@example.com'
                 grav = gravatar_url(email_address=em, size=24)
                 assert grav == 'https://example.com/%s/%s' % (_md5(em), 24)
--- a/kallithea/tests/test.ini	Sun Jan 15 20:57:47 2017 +0100
+++ b/kallithea/tests/test.ini	Sun Jan 29 21:08:49 2017 +0100
@@ -517,7 +517,7 @@
 ################################
 
 [loggers]
-keys = root, routes, kallithea, sqlalchemy, gearbox, beaker, templates, whoosh_indexer
+keys = root, routes, kallithea, sqlalchemy, tg, gearbox, beaker, templates, whoosh_indexer
 
 [handlers]
 keys = console, console_sql
@@ -558,6 +558,12 @@
 qualname = kallithea
 propagate = 1
 
+[logger_tg]
+level = DEBUG
+handlers =
+qualname = tg
+propagate = 1
+
 [logger_gearbox]
 level = DEBUG
 handlers =
--- a/setup.py	Sun Jan 15 20:57:47 2017 +0100
+++ b/setup.py	Sun Jan 29 21:08:49 2017 +0100
@@ -39,7 +39,9 @@
     "GearBox<1",
     "waitress>=0.8.8,<1.0",
     "webob>=1.7,<2",
-    "Pylons>=1.0.0,<=1.0.2",
+    "backlash >= 0.1.1, < 1.0.0",
+    "TurboGears2 >= 2.3.10, < 3.0.0",
+    "tgext.routes >= 0.2.0, < 1.0.0",
     "Beaker>=1.7.0,<2",
     "WebHelpers==1.3",
     "formencode>=1.2.4,<=1.2.6",
@@ -56,6 +58,8 @@
     "Routes==1.13",
     "dulwich>=0.14.1",
     "mercurial>=2.9,<4.2",
+    "decorator >= 3.3.2",
+    "Paste >= 2.0.3, < 3.0",
 ]
 
 if sys.version_info < (2, 7):
@@ -151,9 +155,6 @@
     [paste.app_factory]
     main = kallithea.config.middleware:make_app
 
-    [paste.app_install]
-    main = pylons.util:PylonsInstaller
-
     [gearbox.commands]
     make-config=kallithea.lib.paster_commands.make_config:Command
     setup-db=kallithea.lib.paster_commands.setup_db:Command