diff --git a/dev_requirements.txt b/dev_requirements.txt
--- a/dev_requirements.txt
+++ b/dev_requirements.txt
@@ -6,3 +6,4 @@ pytest-sugar>=0.7.0
pytest-catchlog
mock
sphinx
+webtest < 3
diff --git a/development.ini b/development.ini
--- a/development.ini
+++ b/development.ini
@@ -512,7 +512,7 @@ script_location = kallithea:alembic
################################
[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 @@ handlers =
qualname = kallithea
propagate = 1
+[logger_tg]
+level = DEBUG
+handlers =
+qualname = tg
+propagate = 1
+
[logger_gearbox]
level = DEBUG
handlers =
diff --git a/kallithea/__init__.py b/kallithea/__init__.py
--- a/kallithea/__init__.py
+++ b/kallithea/__init__.py
@@ -30,10 +30,6 @@ Original author and date, and relevant c
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',
diff --git a/kallithea/config/app_cfg.py b/kallithea/config/app_cfg.py
--- a/kallithea/config/app_cfg.py
+++ b/kallithea/config/app_cfg.py
@@ -11,76 +11,112 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+"""
+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 @@ def setup_configuration(config, paths, a
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 @@ def setup_configuration(config, paths, a
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)
diff --git a/kallithea/config/environment.py b/kallithea/config/environment.py
--- a/kallithea/config/environment.py
+++ b/kallithea/config/environment.py
@@ -11,34 +11,11 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-"""
- 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()
diff --git a/kallithea/config/middleware.py b/kallithea/config/middleware.py
--- a/kallithea/config/middleware.py
+++ b/kallithea/config/middleware.py
@@ -11,33 +11,35 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-"""
- 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:] section of the Paste ini file (where
- 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
diff --git a/kallithea/config/routing.py b/kallithea/config/routing.py
--- a/kallithea/config/routing.py
+++ b/kallithea/config/routing.py
@@ -28,7 +28,7 @@ ADMIN_PREFIX = '/_admin'
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 @@ def make_map(config):
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 make_map(config):
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 @@ def make_map(config):
#==========================================================================
# 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
diff --git a/kallithea/controllers/admin/repo_groups.py b/kallithea/controllers/admin/repo_groups.py
--- a/kallithea/controllers/admin/repo_groups.py
+++ b/kallithea/controllers/admin/repo_groups.py
@@ -32,7 +32,7 @@ import itertools
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 @@ class RepoGroupsController(BaseControlle
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: (
diff --git a/kallithea/controllers/admin/user_groups.py b/kallithea/controllers/admin/user_groups.py
--- a/kallithea/controllers/admin/user_groups.py
+++ b/kallithea/controllers/admin/user_groups.py
@@ -30,7 +30,7 @@ import traceback
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 @@ class UserGroupsController(BaseControlle
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: (
diff --git a/kallithea/controllers/admin/users.py b/kallithea/controllers/admin/users.py
--- a/kallithea/controllers/admin/users.py
+++ b/kallithea/controllers/admin/users.py
@@ -30,7 +30,7 @@ import traceback
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 @@ class UsersController(BaseController):
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 = '%s
'
diff --git a/kallithea/controllers/api/__init__.py b/kallithea/controllers/api/__init__.py
--- a/kallithea/controllers/api/__init__.py
+++ b/kallithea/controllers/api/__init__.py
@@ -32,12 +32,9 @@ import traceback
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 @@ class JSONRPCError(BaseException):
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 @@ class JSONRPCController(WSGIController):
"""
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 @@ class JSONRPCController(WSGIController):
)
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:
diff --git a/kallithea/controllers/error.py b/kallithea/controllers/error.py
--- a/kallithea/controllers/error.py
+++ b/kallithea/controllers/error.py
@@ -29,9 +29,8 @@ import os
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 @@ class ErrorController(BaseController):
# 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 @@ class ErrorController(BaseController):
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
diff --git a/kallithea/controllers/root.py b/kallithea/controllers/root.py
new file mode 100644
--- /dev/null
+++ b/kallithea/controllers/root.py
@@ -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 .
+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()
diff --git a/kallithea/lib/app_globals.py b/kallithea/lib/app_globals.py
--- a/kallithea/lib/app_globals.py
+++ b/kallithea/lib/app_globals.py
@@ -25,9 +25,8 @@ Original author and date, and relevant c
: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 @@ class Globals(object):
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
diff --git a/kallithea/lib/base.py b/kallithea/lib/base.py
--- a/kallithea/lib/base.py
+++ b/kallithea/lib/base.py
@@ -41,9 +41,8 @@ import paste.auth.basic
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 @@ from kallithea.model.scm import ScmModel
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_ip_addr(environ):
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 @@ class BaseVCSController(object):
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 @@ class BaseController(WSGIController):
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 @@ class BaseController(WSGIController):
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 @@ class BaseController(WSGIController):
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):
diff --git a/kallithea/lib/paster_commands/common.py b/kallithea/lib/paster_commands/common.py
--- a/kallithea/lib/paster_commands/common.py
+++ b/kallithea/lib/paster_commands/common.py
@@ -82,12 +82,12 @@ class BasePasterCommand(gearbox.command.
"""
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):
"""
diff --git a/kallithea/lib/paster_commands/make_index.py b/kallithea/lib/paster_commands/make_index.py
--- a/kallithea/lib/paster_commands/make_index.py
+++ b/kallithea/lib/paster_commands/make_index.py
@@ -40,7 +40,7 @@ class Command(BasePasterCommand):
"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'])
diff --git a/kallithea/lib/paster_commands/make_rcextensions.py b/kallithea/lib/paster_commands/make_rcextensions.py
--- a/kallithea/lib/paster_commands/make_rcextensions.py
+++ b/kallithea/lib/paster_commands/make_rcextensions.py
@@ -44,7 +44,7 @@ class Command(BasePasterCommand):
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(
diff --git a/kallithea/lib/paster_commands/template.ini.mako b/kallithea/lib/paster_commands/template.ini.mako
--- a/kallithea/lib/paster_commands/template.ini.mako
+++ b/kallithea/lib/paster_commands/template.ini.mako
@@ -516,7 +516,7 @@ script_location = kallithea:alembic
<%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 @@ handlers =
qualname = kallithea
propagate = 1
+[logger_tg]
+level = DEBUG
+handlers =
+qualname = tg
+propagate = 1
+
[logger_gearbox]
level = DEBUG
handlers =
diff --git a/kallithea/lib/utils.py b/kallithea/lib/utils.py
--- a/kallithea/lib/utils.py
+++ b/kallithea/lib/utils.py
@@ -32,6 +32,7 @@ import datetime
import traceback
import beaker
+from tg import request, response
from webhelpers.text import collapse, remove_formatting, strip_tags
from beaker.cache import _cache_decorate
diff --git a/kallithea/model/notification.py b/kallithea/model/notification.py
--- a/kallithea/model/notification.py
+++ b/kallithea/model/notification.py
@@ -29,7 +29,7 @@ Original author and date, and relevant c
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 @@ class EmailNotificationModel(object):
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',
diff --git a/kallithea/model/repo.py b/kallithea/model/repo.py
--- a/kallithea/model/repo.py
+++ b/kallithea/model/repo.py
@@ -153,10 +153,10 @@ class RepoModel(object):
@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)
diff --git a/kallithea/tests/base.py b/kallithea/tests/base.py
--- a/kallithea/tests/base.py
+++ b/kallithea/tests/base.py
@@ -21,14 +21,9 @@ import tempfile
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 @@ log = logging.getLogger(__name__)
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 @@ class TestController(object):
@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):
diff --git a/kallithea/tests/conftest.py b/kallithea/tests/conftest.py
--- a/kallithea/tests/conftest.py
+++ b/kallithea/tests/conftest.py
@@ -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 @@ def pytest_configure():
# 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
diff --git a/kallithea/tests/functional/test_admin_notifications.py b/kallithea/tests/functional/test_admin_notifications.py
--- a/kallithea/tests/functional/test_admin_notifications.py
+++ b/kallithea/tests/functional/test_admin_notifications.py
@@ -6,7 +6,7 @@ from kallithea.model.notification import
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):
diff --git a/kallithea/tests/functional/test_admin_permissions.py b/kallithea/tests/functional/test_admin_permissions.py
--- a/kallithea/tests/functional/test_admin_permissions.py
+++ b/kallithea/tests/functional/test_admin_permissions.py
@@ -5,7 +5,7 @@ from kallithea.model.user import UserMod
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):
diff --git a/kallithea/tests/functional/test_admin_users.py b/kallithea/tests/functional/test_admin_users.py
--- a/kallithea/tests/functional/test_admin_users.py
+++ b/kallithea/tests/functional/test_admin_users.py
@@ -17,7 +17,6 @@ from sqlalchemy.orm.exc import NoResultF
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.lib import helpers as h
from kallithea.model.meta import Session
from webob.exc import HTTPNotFound
+from tg.util.webtest import test_context
+
fixture = Fixture()
@pytest.fixture
diff --git a/kallithea/tests/functional/test_login.py b/kallithea/tests/functional/test_login.py
--- a/kallithea/tests/functional/test_login.py
+++ b/kallithea/tests/functional/test_login.py
@@ -16,7 +16,7 @@ from kallithea.model.db import User, Not
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()
diff --git a/kallithea/tests/functional/test_my_account.py b/kallithea/tests/functional/test_my_account.py
--- a/kallithea/tests/functional/test_my_account.py
+++ b/kallithea/tests/functional/test_my_account.py
@@ -7,7 +7,7 @@ from kallithea.lib import helpers as h
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()
diff --git a/kallithea/tests/functional/test_pullrequests.py b/kallithea/tests/functional/test_pullrequests.py
--- a/kallithea/tests/functional/test_pullrequests.py
+++ b/kallithea/tests/functional/test_pullrequests.py
@@ -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
diff --git a/kallithea/tests/models/test_notifications.py b/kallithea/tests/models/test_notifications.py
--- a/kallithea/tests/models/test_notifications.py
+++ b/kallithea/tests/models/test_notifications.py
@@ -14,7 +14,7 @@ from kallithea.model.notification import
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):
diff --git a/kallithea/tests/other/test_libs.py b/kallithea/tests/other/test_libs.py
--- a/kallithea/tests/other/test_libs.py
+++ b/kallithea/tests/other/test_libs.py
@@ -31,7 +31,7 @@ import mock
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 @@ class TestLibs(TestController):
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 @@ class TestLibs(TestController):
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)
diff --git a/kallithea/tests/test.ini b/kallithea/tests/test.ini
--- a/kallithea/tests/test.ini
+++ b/kallithea/tests/test.ini
@@ -517,7 +517,7 @@ script_location = kallithea:alembic
################################
[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 @@ handlers =
qualname = kallithea
propagate = 1
+[logger_tg]
+level = DEBUG
+handlers =
+qualname = tg
+propagate = 1
+
[logger_gearbox]
level = DEBUG
handlers =
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -39,7 +39,9 @@ requirements = [
"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 @@ requirements = [
"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 @@ setuptools.setup(
[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