From 8ca1a87201fd2ab7b56399af27d2da0f22bc08b8 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 2 Sep 2011 15:31:36 +0200 Subject: [PATCH] [IMP] wsgi: modules can be pre-loaded and expose a WSGI handler. An option --load is added to list the modules to pre-load. sys.path and sys.modules are initialized when the config is parsed. bzr revid: vmt@openerp.com-20110902133136-4v7fgptyd0g1kc5s --- openerp-server | 10 +++++++++ openerp/addons/__init__.py | 13 +++++++---- openerp/addons/base/__init__.py | 5 +++++ openerp/addons/base/__openerp__.py | 1 + openerp/conf/__init__.py | 13 +++++++++++ openerp/modules/graph.py | 23 ++++++++++--------- openerp/modules/module.py | 32 ++++++++++++++------------ openerp/tools/config.py | 36 ++++++++++++++++++++++++++++-- openerp/wsgi.py | 22 +++++++++++++++++- 9 files changed, 124 insertions(+), 31 deletions(-) diff --git a/openerp-server b/openerp-server index 2698307ff42..bc4a661b08a 100755 --- a/openerp-server +++ b/openerp-server @@ -259,6 +259,16 @@ if __name__ == "__main__": if config["stop_after_init"]: sys.exit(0) + for m in openerp.conf.server_wide_modules: + __import__(m) + # Register a WSGI entry point if any. + info = openerp.modules.module.load_information_from_description_file(m) + if info['wsgi']: + openerp.wsgi.register_wsgi_handler(getattr(sys.modules[m], info['wsgi'])) + + openerp.wsgi.serve() + + setup_pid_file() setup_signal_handlers() start_services() diff --git a/openerp/addons/__init__.py b/openerp/addons/__init__.py index 88a0d113151..1f863eca509 100644 --- a/openerp/addons/__init__.py +++ b/openerp/addons/__init__.py @@ -22,10 +22,15 @@ """ Addons module. -This module only serves to contain OpenERP addons. For the code to -manage those addons, see openerp.modules. This module conveniently -reexports some symbols from openerp.modules. Importing them from here -is deprecated. +This module serves to contain all OpenERP addons, across all configured addons +paths. For the code to manage those addons, see openerp.modules. + +Addons are made available here (i.e. under openerp.addons) after +openerp.tools.config.parse_config() is called (so that the addons paths +are known). + +This module also conveniently reexports some symbols from openerp.modules. +Importing them from here is deprecated. """ diff --git a/openerp/addons/base/__init__.py b/openerp/addons/base/__init__.py index 847bef71f8c..457c46f7887 100644 --- a/openerp/addons/base/__init__.py +++ b/openerp/addons/base/__init__.py @@ -25,5 +25,10 @@ import res import publisher_warranty import report +def YEAH(environ, start_response): + response = 'YEAH.\n' + start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))]) + return [response] + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/__openerp__.py b/openerp/addons/base/__openerp__.py index 37f26edc177..f19c5bc7832 100644 --- a/openerp/addons/base/__openerp__.py +++ b/openerp/addons/base/__openerp__.py @@ -93,6 +93,7 @@ 'test/test_ir_rule.yml', # <-- These tests modify/add/delete ir_rules. 'test/test_ir_values.yml', ], + 'wsgi': 'YEAH', 'installable': True, 'active': True, 'certificate': '0076807797149', diff --git a/openerp/conf/__init__.py b/openerp/conf/__init__.py index 7de8070f9e2..0a975c5d4e2 100644 --- a/openerp/conf/__init__.py +++ b/openerp/conf/__init__.py @@ -28,8 +28,21 @@ parsing, configuration file loading and saving, ...) in this module and provide real Python variables, e.g. addons_paths is really a list of paths. +To initialize properly this module, openerp.tools.config.parse_config() +must be used. + """ import deprecation +# Paths to search for OpenERP addons. +addons_paths = [] + +# List of server-wide modules to load. Those modules are supposed to provide +# features not necessarily tied to a particular database. This is in contrast +# to modules that are always bound to a specific database when they are +# installed (i.e. the majority of OpenERP addons). This is set with the --load +# command-line option. +server_wide_modules = [] + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/modules/graph.py b/openerp/modules/graph.py index 078d23c235b..539dda25b50 100644 --- a/openerp/modules/graph.py +++ b/openerp/modules/graph.py @@ -58,16 +58,16 @@ class Graph(dict): """ - def add_node(self, name, deps): + def add_node(self, name, info): max_depth, father = 0, None - for n in [Node(x, self) for x in deps]: + for n in [Node(x, self, None) for x in info['depends']]: if n.depth >= max_depth: father = n max_depth = n.depth if father: - return father.add_child(name) + return father.add_child(name, info) else: - return Node(name, self) + return Node(name, self, info) def update_from_db(self, cr): if not len(self): @@ -120,7 +120,7 @@ class Graph(dict): continue later.clear() current.remove(package) - node = self.add_node(package, deps) + node = self.add_node(package, info) node.data = info for kind in ('init', 'demo', 'update'): if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force: @@ -154,12 +154,13 @@ class Graph(dict): class Singleton(object): - def __new__(cls, name, graph): + def __new__(cls, name, graph, info): if name in graph: inst = graph[name] else: inst = object.__new__(cls) inst.name = name + inst.info = info graph[name] = inst return inst @@ -167,19 +168,21 @@ class Singleton(object): class Node(Singleton): """ One module in the modules dependency graph. - Node acts as a per-module singleton. + Node acts as a per-module singleton. A node is constructed via + Graph.add_module() or Graph.add_modules(). Some of its fields are from + ir_module_module (setted by Graph.update_from_db()). """ - def __init__(self, name, graph): + def __init__(self, name, graph, info): self.graph = graph if not hasattr(self, 'children'): self.children = [] if not hasattr(self, 'depth'): self.depth = 0 - def add_child(self, name): - node = Node(name, self.graph) + def add_child(self, name, info): + node = Node(name, self.graph, info) node.depth = self.depth + 1 if node not in self.children: self.children.append(node) diff --git a/openerp/modules/module.py b/openerp/modules/module.py index 1514e836dd8..cd5e6537a6d 100644 --- a/openerp/modules/module.py +++ b/openerp/modules/module.py @@ -31,7 +31,6 @@ import openerp.osv as osv import openerp.tools as tools import openerp.tools.osutil as osutil from openerp.tools.safe_eval import safe_eval as eval -import openerp.pooler as pooler from openerp.tools.translate import _ import openerp.netsvc as netsvc @@ -58,6 +57,11 @@ loaded = [] logger = netsvc.Logger() def initialize_sys_path(): + """ Add all addons paths in sys.path. + + This ensures something like ``import crm`` works even if the addons are + not in the PYTHONPATH. + """ global ad_paths if ad_paths: @@ -250,6 +254,7 @@ def load_information_from_description_file(module): info['license'] = info.get('license') or 'AGPL-3' info.setdefault('installable', True) info.setdefault('active', False) + info.setdefault('wsgi', None) # WSGI entry point, given as a string for kind in ['data', 'demo', 'test', 'init_xml', 'update_xml', 'demo_xml']: info.setdefault(kind, []) @@ -290,23 +295,22 @@ def init_module_models(cr, module_name, obj_list): t[1](cr, *t[2]) cr.commit() +# Import hook to write a addon m in both sys.modules['m'] and +# sys.modules['openerp.addons.m']. Otherwise it could be loaded twice +# if imported twice using different names. +#class MyImportHook(object): +# def find_module(self, module_name, package_path): +# print ">>>", module_name, package_path +# def load_module(self, module_name): +# raise ImportError("Restricted") -def load_module(module_name): - """ Load a Python module found on the addons paths.""" - fm = imp.find_module(module_name, ad_paths) - try: - imp.load_module(module_name, *fm) - finally: - if fm[0]: - fm[0].close() - +#sys.meta_path.append(MyImportHook()) def register_module_classes(m): """ Register module named m, if not already registered. - This will load the module and register all of its models. (Actually, the - explicit constructor call of each of the models inside the module will - register them.) + This loads the module and register all of its models, thanks to either + the MetaModel metaclass, or the explicit instantiation of the model. """ @@ -326,7 +330,7 @@ def register_module_classes(m): try: zip_mod_path = mod_path + '.zip' if not os.path.isfile(zip_mod_path): - load_module(m) + __import__(m) else: zimp = zipimport.zipimporter(zip_mod_path) zimp.load_module(m) diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 18f40e0011f..f5a45dd67a0 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -24,6 +24,7 @@ import optparse import os import sys import openerp +import openerp.conf import openerp.loglevels as loglevels import logging import openerp.release as release @@ -101,8 +102,10 @@ class configmanager(object): group.add_option("-P", "--import-partial", dest="import_partial", my_default='', help="Use this for big data importation, if it crashes you will be able to continue at the current state. Provide a filename to store intermediate importation states.") group.add_option("--pidfile", dest="pidfile", help="file where the server pid will be stored") + group.add_option("--load", dest="server_wide_modules", help="Comma-separated list of server-wide modules") parser.add_option_group(group) + # XML-RPC / HTTP group = optparse.OptionGroup(parser, "XML-RPC Configuration") group.add_option("--xmlrpc-interface", dest="xmlrpc_interface", my_default='', help="Specify the TCP IP address for the XML-RPC protocol. The empty string binds to all interfaces.") @@ -112,6 +115,7 @@ class configmanager(object): help="disable the XML-RPC protocol") parser.add_option_group(group) + # XML-RPC / HTTPS title = "XML-RPC Secure Configuration" if not self.has_ssl: title += " (disabled as ssl is unavailable)" @@ -258,9 +262,26 @@ class configmanager(object): self.options[option.dest] = option.my_default self.casts[option.dest] = option - self.parse_config() + self.parse_config(None, False) - def parse_config(self, args=None): + def parse_config(self, args=None, complete=True): + """ Parse the configuration file (if any) and the command-line + arguments. + + This method initializes openerp.tools.config and openerp.conf (the + former should be removed in the furture) with library-wide + configuration values. + + This method must be called before proper usage of this library can be + made. + + Typical usage of this method: + + openerp.tools.config.parse_config(sys.argv[1:]) + + :param complete: this is a hack used in __init__(), leave it to True. + + """ if args is None: args = [] opt, args = self.parser.parse_args(args) @@ -417,6 +438,17 @@ class configmanager(object): if opt.save: self.save() + openerp.conf.addons_paths = self.options['addons_path'].split(',') + openerp.conf.server_wide_modules = \ + map(lambda m: m.strip(), opt.server_wide_modules.split(',')) if \ + opt.server_wide_modules else [] + if complete: + openerp.modules.module.initialize_sys_path() + openerp.modules.loading.open_openerp_namespace() + # openerp.addons.__path__.extend(openerp.conf.addons_paths) # This + # is not compatible with initialize_sys_path(): import crm and + # import openerp.addons.crm load twice the module. + def _generate_pgpassfile(self): """ Generate the pgpass file with the parameters from the command line (db_host, db_user, diff --git a/openerp/wsgi.py b/openerp/wsgi.py index 93bf7182785..ef3d6d033c6 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -95,11 +95,31 @@ def legacy_wsgi_xmlrpc(environ, start_response): def wsgi_jsonrpc(environ, start_response): pass +def wsgi_modules(environ, start_response): + """ WSGI handler dispatching to addons-provided entry points.""" + pass + +# WSGI handlers provided by modules loaded with the --load command-line option. +module_handlers = [] + +def register_wsgi_handler(handler): + """ Register a WSGI handler. + + Handlers are tried in the order they are added. We might provide a way to + register a handler for specific routes later. + """ + module_handlers.append(handler) + def application(environ, start_response): """ WSGI entry point.""" # Try all handlers until one returns some result (i.e. not None). - wsgi_handlers = [wsgi_xmlrpc, wsgi_jsonrpc, legacy_wsgi_xmlrpc] + wsgi_handlers = [ + wsgi_xmlrpc, + wsgi_jsonrpc, + legacy_wsgi_xmlrpc, + wsgi_modules, + ] + module_handlers for handler in wsgi_handlers: result = handler(environ, start_response) if result is None: