Merge branch 'master' of https://github.com/odoo/odoo
[odoo/odoo.git] / openerp / modules / module.py
index b0b7e0a..e6f6eed 100644 (file)
@@ -27,6 +27,7 @@ import logging
 import os
 import re
 import sys
+import time
 import unittest
 from os.path import join as opj
 
@@ -37,10 +38,14 @@ import openerp.tools as tools
 import openerp.release as release
 from openerp.tools.safe_eval import safe_eval as eval
 
+MANIFEST = '__openerp__.py'
+README = ['README.rst', 'README.md', 'README.txt']
+
 _logger = logging.getLogger(__name__)
 
 # addons path as a list
 ad_paths = []
+hooked = False
 
 # Modules already loaded
 loaded = []
@@ -83,17 +88,25 @@ def initialize_sys_path():
     PYTHONPATH.
     """
     global ad_paths
-    if ad_paths:
-        return
+    global hooked
 
-    ad_paths = [tools.config.addons_data_dir]
-    ad_paths += map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(','))
+    dd = tools.config.addons_data_dir
+    if dd not in ad_paths:
+        ad_paths.append(dd)
+
+    for ad in tools.config['addons_path'].split(','):
+        ad = os.path.abspath(tools.ustr(ad.strip()))
+        if ad not in ad_paths:
+            ad_paths.append(ad)
 
     # add base module path
     base_path = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons'))
-    ad_paths += [base_path]
+    if base_path not in ad_paths:
+        ad_paths.append(base_path)
 
-    sys.meta_path.append(AddonsImportHook())
+    if not hooked:
+        sys.meta_path.append(AddonsImportHook())
+        hooked = True
 
 def get_module_path(module, downloaded=False, display_warning=True):
     """Return the path of the given module.
@@ -142,7 +155,7 @@ def get_module_filetree(module, dir='.'):
 
     return tree
 
-def get_module_resource(module, *args):
+def get_resource_path(module, *args):
     """Return the full path of a resource of the given module.
 
     :param module: module name
@@ -151,7 +164,6 @@ def get_module_resource(module, *args):
     :rtype: str
     :return: absolute path to the resource
 
-    TODO name it get_resource_path
     TODO make it available inside on osv object (self.get_resource_path)
     """
     mod_path = get_module_path(module)
@@ -163,12 +175,66 @@ def get_module_resource(module, *args):
             return resource_path
     return False
 
+# backwards compatibility
+get_module_resource = get_resource_path
+
+def get_resource_from_path(path):
+    """Tries to extract the module name and the resource's relative path
+    out of an absolute resource path.
+
+    If operation is successfull, returns a tuple containing the module name, the relative path
+    to the resource using '/' as filesystem seperator[1] and the same relative path using
+    os.path.sep seperators.
+
+    [1] same convention as the resource path declaration in manifests
+
+    :param path: absolute resource path
+
+    :rtype: tuple
+    :return: tuple(module_name, relative_path, os_relative_path) if possible, else None
+    """
+    resource = [path.replace(adpath, '') for adpath in ad_paths if path.startswith(adpath)]
+    if resource:
+        relative = resource[0].split(os.path.sep)
+        if not relative[0]:
+            relative.pop(0)
+        module = relative.pop(0)
+        return (module, '/'.join(relative), os.path.sep.join(relative))
+    return None
+
 def get_module_icon(module):
     iconpath = ['static', 'description', 'icon.png']
     if get_module_resource(module, *iconpath):
         return ('/' + module + '/') + '/'.join(iconpath)
     return '/base/'  + '/'.join(iconpath)
 
+def get_module_root(path):
+    """
+    Get closest module's root begining from path
+
+        # Given:
+        # /foo/bar/module_dir/static/src/...
+
+        get_module_root('/foo/bar/module_dir/static/')
+        # returns '/foo/bar/module_dir'
+
+        get_module_root('/foo/bar/module_dir/')
+        # returns '/foo/bar/module_dir'
+
+        get_module_root('/foo/bar')
+        # returns None
+
+    @param path: Path from which the lookup should start
+
+    @return:  Module root path or None if not found
+    """
+    while not os.path.exists(os.path.join(path, MANIFEST)):
+        new_path = os.path.abspath(os.path.join(path, os.pardir))
+        if path == new_path:
+            return None
+        path = new_path
+    return path
+
 def load_information_from_description_file(module, mod_path=None):
     """
     :param module: The name of the module (sale, purchase, ...)
@@ -177,7 +243,7 @@ def load_information_from_description_file(module, mod_path=None):
 
     if not mod_path:
         mod_path = get_module_path(module)
-    terp_file = opj(mod_path, '__openerp__.py')
+    terp_file = mod_path and opj(mod_path, MANIFEST) or False
     if terp_file:
         info = {}
         if os.path.isfile(terp_file):
@@ -192,7 +258,6 @@ def load_information_from_description_file(module, mod_path=None):
                 'icon': get_module_icon(module),
                 'installable': True,
                 'license': 'AGPL-3',
-                'name': False,
                 'post_load': None,
                 'version': '1.0',
                 'web': False,
@@ -210,6 +275,13 @@ def load_information_from_description_file(module, mod_path=None):
             finally:
                 f.close()
 
+            if not info.get('description'):
+                readme_path = [opj(mod_path, x) for x in README
+                               if os.path.isfile(opj(mod_path, x))]
+                if readme_path:
+                    readme_text = tools.file_open(readme_path[0]).read()
+                    info['description'] = readme_text
+
             if 'active' in info:
                 # 'active' has been renamed 'auto_install'
                 info['auto_install'] = info['active']
@@ -219,7 +291,7 @@ def load_information_from_description_file(module, mod_path=None):
 
     #TODO: refactor the logger in this file to follow the logging guidelines
     #      for 6.0
-    _logger.debug('module %s: no __openerp__.py file found.', module)
+    _logger.debug('module %s: no %s file found.', module, MANIFEST)
     return {}
 
 def init_module_models(cr, module_name, obj_list):
@@ -243,7 +315,7 @@ def init_module_models(cr, module_name, obj_list):
     for obj in obj_list:
         obj._auto_end(cr, {'module': module_name})
         cr.commit()
-    todo.sort()
+    todo.sort(key=lambda x: x[0])
     for t in todo:
         t[1](cr, *t[2])
     cr.commit()
@@ -291,7 +363,7 @@ def get_modules():
             return name
 
         def is_really_module(name):
-            manifest_name = opj(dir, name, '__openerp__.py')
+            manifest_name = opj(dir, name, MANIFEST)
             zipfile_name = opj(dir, name)
             return os.path.isfile(manifest_name)
         return map(clean, filter(is_really_module, os.listdir(dir)))
@@ -350,38 +422,15 @@ class TestStream(object):
         if self.r.match(s):
             return
         first = True
-        for c in s.split('\n'):
+        level = logging.ERROR if s.startswith(('ERROR', 'FAIL', 'Traceback')) else logging.INFO
+        for c in s.splitlines():
             if not first:
                 c = '` ' + c
             first = False
-            self.logger.info(c)
+            self.logger.log(level, c)
 
 current_test = None
 
-def run_unit_tests(module_name, dbname):
-    """
-    :returns: ``True`` if all of ``module_name``'s tests succeeded, ``False``
-              if any of them failed.
-    :rtype: bool
-    """
-    global current_test
-    current_test = module_name
-    mods = get_test_modules(module_name)
-    r = True
-    for m in mods:
-        tests = unwrap_suite(unittest2.TestLoader().loadTestsFromModule(m))
-        suite = unittest2.TestSuite(itertools.ifilter(runs_at_install, tests))
-        _logger.info('running %s tests.', m.__name__)
-
-        result = unittest2.TextTestRunner(verbosity=2, stream=TestStream(m.__name__)).run(suite)
-
-        if not result.wasSuccessful():
-            r = False
-            _logger.error("Module %s: %d failures, %d errors",
-                          module_name, len(result.failures), len(result.errors))
-    current_test = None
-    return r
-
 def runs_at(test, hook, default):
     # by default, tests do not run post install
     test_runs = getattr(test, hook, default)
@@ -398,6 +447,34 @@ def runs_at(test, hook, default):
 runs_at_install = functools.partial(runs_at, hook='at_install', default=True)
 runs_post_install = functools.partial(runs_at, hook='post_install', default=False)
 
+def run_unit_tests(module_name, dbname, position=runs_at_install):
+    """
+    :returns: ``True`` if all of ``module_name``'s tests succeeded, ``False``
+              if any of them failed.
+    :rtype: bool
+    """
+    global current_test
+    current_test = module_name
+    mods = get_test_modules(module_name)
+    r = True
+    for m in mods:
+        tests = unwrap_suite(unittest2.TestLoader().loadTestsFromModule(m))
+        suite = unittest2.TestSuite(itertools.ifilter(position, tests))
+
+        if suite.countTestCases():
+            t0 = time.time()
+            t0_sql = openerp.sql_db.sql_counter
+            _logger.info('%s running tests.', m.__name__)
+            result = unittest2.TextTestRunner(verbosity=2, stream=TestStream(m.__name__)).run(suite)
+            if time.time() - t0 > 5:
+                _logger.log(25, "%s tested in %.2fs, %s queries", m.__name__, time.time() - t0, openerp.sql_db.sql_counter - t0_sql)
+            if not result.wasSuccessful():
+                r = False
+                _logger.error("Module %s: %d failures, %d errors", module_name, len(result.failures), len(result.errors))
+
+    current_test = None
+    return r
+
 def unwrap_suite(test):
     """
     Attempts to unpack testsuites (holding suites or cases) in order to