1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 # Copyright (C) 2010-2014 OpenERP s.a. (<http://openerp.com>).
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Affero General Public License as
10 # published by the Free Software Foundation, either version 3 of the
11 # License, or (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Affero General Public License for more details.
18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
32 from os.path import join as opj
37 import openerp.tools as tools
38 import openerp.release as release
39 from openerp.tools.safe_eval import safe_eval as eval
41 MANIFEST = '__openerp__.py'
42 README = ['README.rst', 'README.md', 'README.txt']
44 _logger = logging.getLogger(__name__)
46 # addons path as a list
50 # Modules already loaded
53 class AddonsImportHook(object):
55 Import hook to load OpenERP addons from multiple paths.
57 OpenERP implements its own import-hook to load its addons. OpenERP
58 addons are Python modules. Originally, they were each living in their
59 own top-level namespace, e.g. the sale module, or the hr module. For
60 backward compatibility, `import <module>` is still supported. Now they
61 are living in `openerp.addons`. The good way to import such modules is
62 thus `import openerp.addons.module`.
65 def find_module(self, module_name, package_path):
66 module_parts = module_name.split('.')
67 if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
68 return self # We act as a loader too.
70 def load_module(self, module_name):
71 if module_name in sys.modules:
72 return sys.modules[module_name]
74 _1, _2, module_part = module_name.split('.')
75 # Note: we don't support circular import.
76 f, path, descr = imp.find_module(module_part, ad_paths)
77 mod = imp.load_module('openerp.addons.' + module_part, f, path, descr)
78 sys.modules['openerp.addons.' + module_part] = mod
81 def initialize_sys_path():
83 Setup an import-hook to be able to import OpenERP addons from the different
86 This ensures something like ``import crm`` (or even
87 ``import openerp.addons.crm``) works even if the addons are not in the
93 dd = tools.config.addons_data_dir
94 if dd not in ad_paths:
97 for ad in tools.config['addons_path'].split(','):
98 ad = os.path.abspath(tools.ustr(ad.strip()))
99 if ad not in ad_paths:
102 # add base module path
103 base_path = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons'))
104 if base_path not in ad_paths:
105 ad_paths.append(base_path)
108 sys.meta_path.append(AddonsImportHook())
111 def get_module_path(module, downloaded=False, display_warning=True):
112 """Return the path of the given module.
114 Search the addons paths and return the first path where the given
115 module is found. If downloaded is True, return the default addons
116 path if nothing else is found.
119 initialize_sys_path()
121 if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
122 return opj(adp, module)
125 return opj(tools.config.addons_data_dir, module)
127 _logger.warning('module %s: module not found', module)
130 def get_module_filetree(module, dir='.'):
131 path = get_module_path(module)
135 dir = os.path.normpath(dir)
138 if dir.startswith('..') or (dir and dir[0] == '/'):
139 raise Exception('Cannot access file outside the module')
141 files = openerp.tools.osutil.listdir(path, True)
145 if not f.startswith(dir):
149 f = f[len(dir)+int(not dir.endswith('/')):]
150 lst = f.split(os.sep)
153 current = current.setdefault(lst.pop(0), {})
154 current[lst.pop(0)] = None
158 def get_module_resource(module, *args):
159 """Return the full path of a resource of the given module.
161 :param module: module name
162 :param list(str) args: resource path components within module
165 :return: absolute path to the resource
167 TODO name it get_resource_path
168 TODO make it available inside on osv object (self.get_resource_path)
170 mod_path = get_module_path(module)
171 if not mod_path: return False
172 resource_path = opj(mod_path, *args)
173 if os.path.isdir(mod_path):
174 # the module is a directory - ignore zip behavior
175 if os.path.exists(resource_path):
179 def get_module_icon(module):
180 iconpath = ['static', 'description', 'icon.png']
181 if get_module_resource(module, *iconpath):
182 return ('/' + module + '/') + '/'.join(iconpath)
183 return '/base/' + '/'.join(iconpath)
185 def get_module_root(path):
187 Get closest module's root begining from path
190 # /foo/bar/module_dir/static/src/...
192 get_module_root('/foo/bar/module_dir/static/')
193 # returns '/foo/bar/module_dir'
195 get_module_root('/foo/bar/module_dir/')
196 # returns '/foo/bar/module_dir'
198 get_module_root('/foo/bar')
201 @param path: Path from which the lookup should start
203 @return: Module root path or None if not found
205 while not os.path.exists(os.path.join(path, MANIFEST)):
206 new_path = os.path.abspath(os.path.join(path, os.pardir))
212 def load_information_from_description_file(module, mod_path=None):
214 :param module: The name of the module (sale, purchase, ...)
215 :param mod_path: Physical path of module, if not providedThe name of the module (sale, purchase, ...)
219 mod_path = get_module_path(module)
220 terp_file = mod_path and opj(mod_path, MANIFEST) or False
223 if os.path.isfile(terp_file):
224 # default values for descriptor
226 'application': False,
228 'auto_install': False,
229 'category': 'Uncategorized',
232 'icon': get_module_icon(module),
242 info.update(itertools.izip(
243 'depends data demo test init_xml update_xml demo_xml'.split(),
246 f = tools.file_open(terp_file)
248 info.update(eval(f.read()))
252 if not info.get('description'):
253 readme_path = [opj(mod_path, x) for x in README
254 if os.path.isfile(opj(mod_path, x))]
256 readme_text = tools.file_open(readme_path[0]).read()
257 info['description'] = readme_text
260 # 'active' has been renamed 'auto_install'
261 info['auto_install'] = info['active']
263 info['version'] = adapt_version(info['version'])
266 #TODO: refactor the logger in this file to follow the logging guidelines
268 _logger.debug('module %s: no %s file found.', module, MANIFEST)
271 def init_module_models(cr, module_name, obj_list):
272 """ Initialize a list of models.
274 Call _auto_init and init on each model to create or update the
275 database tables supporting the models.
277 TODO better explanation of _auto_init and init.
280 _logger.info('module %s: creating or updating database tables', module_name)
283 result = obj._auto_init(cr, {'module': module_name})
286 if hasattr(obj, 'init'):
290 obj._auto_end(cr, {'module': module_name})
292 todo.sort(key=lambda x: x[0])
297 def load_openerp_module(module_name):
298 """ Load an OpenERP module, if not already loaded.
300 This loads the module and register all of its models, thanks to either
301 the MetaModel metaclass, or the explicit instantiation of the model.
302 This is also used to load server-wide module (i.e. it is also used
303 when there is no model to register).
306 if module_name in loaded:
309 initialize_sys_path()
311 mod_path = get_module_path(module_name)
312 __import__('openerp.addons.' + module_name)
314 # Call the module's post-load hook. This can done before any model or
315 # data has been initialized. This is ok as the post-load hook is for
316 # server-wide (instead of registry-specific) functionalities.
317 info = load_information_from_description_file(module_name)
318 if info['post_load']:
319 getattr(sys.modules['openerp.addons.' + module_name], info['post_load'])()
322 msg = "Couldn't load module %s" % (module_name)
323 _logger.critical(msg)
327 loaded.append(module_name)
330 """Returns the list of module names
334 name = os.path.basename(name)
335 if name[-4:] == '.zip':
339 def is_really_module(name):
340 manifest_name = opj(dir, name, MANIFEST)
341 zipfile_name = opj(dir, name)
342 return os.path.isfile(manifest_name)
343 return map(clean, filter(is_really_module, os.listdir(dir)))
346 initialize_sys_path()
348 plist.extend(listdir(ad))
349 return list(set(plist))
351 def get_modules_with_version():
352 modules = get_modules()
353 res = dict.fromkeys(modules, adapt_version('1.0'))
354 for module in modules:
356 info = load_information_from_description_file(module)
357 res[module] = info['version']
362 def adapt_version(version):
363 serie = release.major_version
364 if version == serie or not version.startswith(serie + '.'):
365 version = '%s.%s' % (serie, version)
368 def get_test_modules(module):
369 """ Return a list of module for the addons potentialy containing tests to
370 feed unittest2.TestLoader.loadTestsFromModule() """
371 # Try to import the module
372 module = 'openerp.addons.' + module + '.tests'
376 # If module has no `tests` sub-module, no problem.
377 if str(e) != 'No module named tests':
378 _logger.exception('Can not `import %s`.', module)
381 # include submodules too
382 result = [mod_obj for name, mod_obj in sys.modules.iteritems()
383 if mod_obj # mod_obj can be None
384 if name.startswith(module)
385 if re.search(r'test_\w+$', name)]
388 # Use a custom stream object to log the test executions.
389 class TestStream(object):
390 def __init__(self, logger_name='openerp.tests'):
391 self.logger = logging.getLogger(logger_name)
392 self.r = re.compile(r'^-*$|^ *... *$|^ok$')
399 level = logging.ERROR if s.startswith(('ERROR', 'FAIL', 'Traceback')) else logging.INFO
400 for c in s.splitlines():
404 self.logger.log(level, c)
408 def runs_at(test, hook, default):
409 # by default, tests do not run post install
410 test_runs = getattr(test, hook, default)
412 # for a test suite, we're done
413 if not isinstance(test, unittest.TestCase):
416 # otherwise check the current test method to see it's been set to a
418 method = getattr(test, test._testMethodName)
419 return getattr(method, hook, test_runs)
421 runs_at_install = functools.partial(runs_at, hook='at_install', default=True)
422 runs_post_install = functools.partial(runs_at, hook='post_install', default=False)
424 def run_unit_tests(module_name, dbname, position=runs_at_install):
426 :returns: ``True`` if all of ``module_name``'s tests succeeded, ``False``
427 if any of them failed.
431 current_test = module_name
432 mods = get_test_modules(module_name)
435 tests = unwrap_suite(unittest2.TestLoader().loadTestsFromModule(m))
436 suite = unittest2.TestSuite(itertools.ifilter(position, tests))
438 if suite.countTestCases():
440 t0_sql = openerp.sql_db.sql_counter
441 _logger.info('%s running tests.', m.__name__)
442 result = unittest2.TextTestRunner(verbosity=2, stream=TestStream(m.__name__)).run(suite)
443 if time.time() - t0 > 5:
444 _logger.log(25, "%s tested in %.2fs, %s queries", m.__name__, time.time() - t0, openerp.sql_db.sql_counter - t0_sql)
445 if not result.wasSuccessful():
447 _logger.error("Module %s: %d failures, %d errors", module_name, len(result.failures), len(result.errors))
452 def unwrap_suite(test):
454 Attempts to unpack testsuites (holding suites or cases) in order to
455 generate a single stream of terminals (either test cases or customized
456 test suites). These can then be checked for run/skip attributes
459 An alternative would be to use a variant of @unittest2.skipIf with a state
460 flag of some sort e.g. @unittest2.skipIf(common.runstate != 'at_install'),
461 but then things become weird with post_install as tests should *not* run
464 if isinstance(test, unittest.TestCase):
468 subtests = list(test)
469 # custom test suite (no test cases)
470 if not len(subtests):
474 for item in itertools.chain.from_iterable(
475 itertools.imap(unwrap_suite, subtests)):
478 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: