[IMP] wsgi: modules can be pre-loaded and expose a WSGI handler.
authorVo Minh Thu <vmt@openerp.com>
Fri, 2 Sep 2011 13:31:36 +0000 (15:31 +0200)
committerVo Minh Thu <vmt@openerp.com>
Fri, 2 Sep 2011 13:31:36 +0000 (15:31 +0200)
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
openerp/addons/__init__.py
openerp/addons/base/__init__.py
openerp/addons/base/__openerp__.py
openerp/conf/__init__.py
openerp/modules/graph.py
openerp/modules/module.py
openerp/tools/config.py
openerp/wsgi.py

index 2698307..bc4a661 100755 (executable)
@@ -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()
index 88a0d11..1f863ec 100644 (file)
 
 """ 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.
 
 """
 
index 847bef7..457c46f 100644 (file)
@@ -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:
 
index 37f26ed..f19c5bc 100644 (file)
@@ -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',
index 7de8070..0a975c5 100644 (file)
@@ -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:
index 078d23c..539dda2 100644 (file)
@@ -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)
index 1514e83..cd5e653 100644 (file)
@@ -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)
index 18f40e0..f5a45dd 100644 (file)
@@ -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,
index 93bf718..ef3d6d0 100644 (file)
@@ -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: