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>################################ [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