Mercurial > kallithea
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
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