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'
43 _logger = logging.getLogger(__name__)
45 # addons path as a list
49 # Modules already loaded
52 class AddonsImportHook(object):
54 Import hook to load OpenERP addons from multiple paths.
56 OpenERP implements its own import-hook to load its addons. OpenERP
57 addons are Python modules. Originally, they were each living in their
58 own top-level namespace, e.g. the sale module, or the hr module. For
59 backward compatibility, `import <module>` is still supported. Now they
60 are living in `openerp.addons`. The good way to import such modules is
61 thus `import openerp.addons.module`.
64 def find_module(self, module_name, package_path):
65 module_parts = module_name.split('.')
66 if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
67 return self # We act as a loader too.
69 def load_module(self, module_name):
70 if module_name in sys.modules:
71 return sys.modules[module_name]
73 _1, _2, module_part = module_name.split('.')
74 # Note: we don't support circular import.
75 f, path, descr = imp.find_module(module_part, ad_paths)
76 mod = imp.load_module('openerp.addons.' + module_part, f, path, descr)
77 sys.modules['openerp.addons.' + module_part] = mod
80 def initialize_sys_path():
82 Setup an import-hook to be able to import OpenERP addons from the different
85 This ensures something like ``import crm`` (or even
86 ``import openerp.addons.crm``) works even if the addons are not in the
92 dd = tools.config.addons_data_dir
93 if dd not in ad_paths:
96 for ad in tools.config['addons_path'].split(','):
97 ad = os.path.abspath(tools.ustr(ad.strip()))
98 if ad not in ad_paths:
101 # add base module path
102 base_path = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons'))
103 if base_path not in ad_paths:
104 ad_paths.append(base_path)
107 sys.meta_path.append(AddonsImportHook())
110 def get_module_path(module, downloaded=False, display_warning=True):
111 """Return the path of the given module.
113 Search the addons paths and return the first path where the given
114 module is found. If downloaded is True, return the default addons
115 path if nothing else is found.
118 initialize_sys_path()
120 if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
121 return opj(adp, module)
124 return opj(tools.config.addons_data_dir, module)
126 _logger.warning('module %s: module not found', module)
129 def get_module_filetree(module, dir='.'):
130 path = get_module_path(module)
134 dir = os.path.normpath(dir)
137 if dir.startswith('..') or (dir and dir[0] == '/'):
138 raise Exception('Cannot access file outside the module')
140 files = openerp.tools.osutil.listdir(path, True)
144 if not f.startswith(dir):
148 f = f[len(dir)+int(not dir.endswith('/')):]
149 lst = f.split(os.sep)
152 current = current.setdefault(lst.pop(0), {})
153 current[lst.pop(0)] = None
157 def get_module_resource(module, *args):
158 """Return the full path of a resource of the given module.
160 :param module: module name
161 :param list(str) args: resource path components within module
164 :return: absolute path to the resource
166 TODO name it get_resource_path
167 TODO make it available inside on osv object (self.get_resource_path)
169 mod_path = get_module_path(module)
170 if not mod_path: return False
171 resource_path = opj(mod_path, *args)
172 if os.path.isdir(mod_path):
173 # the module is a directory - ignore zip behavior
174 if os.path.exists(resource_path):
178 def get_module_icon(module):
179 iconpath = ['static', 'description', 'icon.png']
180 if get_module_resource(module, *iconpath):
181 return ('/' + module + '/') + '/'.join(iconpath)
182 return '/base/' + '/'.join(iconpath)
184 def get_module_root(path):
186 Get closest module's root begining from path
189 # /foo/bar/module_dir/static/src/...
191 get_module_root('/foo/bar/module_dir/static/')
192 # returns '/foo/bar/module_dir'
194 get_module_root('/foo/bar/module_dir/')
195 # returns '/foo/bar/module_dir'
197 get_module_root('/foo/bar')
200 @param path: Path from which the lookup should start
202 @return: Module root path or None if not found
204 while not os.path.exists(os.path.join(path, MANIFEST)):
205 new_path = os.path.abspath(os.path.join(path, os.pardir))
211 def load_information_from_description_file(module, mod_path=None):
213 :param module: The name of the module (sale, purchase, ...)
214 :param mod_path: Physical path of module, if not providedThe name of the module (sale, purchase, ...)
218 mod_path = get_module_path(module)
219 terp_file = mod_path and opj(mod_path, MANIFEST) or False
222 if os.path.isfile(terp_file):
223 # default values for descriptor
225 'application': False,
227 'auto_install': False,
228 'category': 'Uncategorized',
231 'icon': get_module_icon(module),
241 info.update(itertools.izip(
242 'depends data demo test init_xml update_xml demo_xml'.split(),
245 f = tools.file_open(terp_file)
247 info.update(eval(f.read()))
252 # 'active' has been renamed 'auto_install'
253 info['auto_install'] = info['active']
255 info['version'] = adapt_version(info['version'])
258 #TODO: refactor the logger in this file to follow the logging guidelines
260 _logger.debug('module %s: no %s file found.', module, MANIFEST)
263 def init_module_models(cr, module_name, obj_list):
264 """ Initialize a list of models.
266 Call _auto_init and init on each model to create or update the
267 database tables supporting the models.
269 TODO better explanation of _auto_init and init.
272 _logger.info('module %s: creating or updating database tables', module_name)
275 result = obj._auto_init(cr, {'module': module_name})
278 if hasattr(obj, 'init'):
282 obj._auto_end(cr, {'module': module_name})
284 todo.sort(key=lambda x: x[0])
289 def load_openerp_module(module_name):
290 """ Load an OpenERP module, if not already loaded.
292 This loads the module and register all of its models, thanks to either
293 the MetaModel metaclass, or the explicit instantiation of the model.
294 This is also used to load server-wide module (i.e. it is also used
295 when there is no model to register).
298 if module_name in loaded:
301 initialize_sys_path()
303 mod_path = get_module_path(module_name)
304 __import__('openerp.addons.' + module_name)
306 # Call the module's post-load hook. This can done before any model or
307 # data has been initialized. This is ok as the post-load hook is for
308 # server-wide (instead of registry-specific) functionalities.
309 info = load_information_from_description_file(module_name)
310 if info['post_load']:
311 getattr(sys.modules['openerp.addons.' + module_name], info['post_load'])()
314 msg = "Couldn't load module %s" % (module_name)
315 _logger.critical(msg)
319 loaded.append(module_name)
322 """Returns the list of module names
326 name = os.path.basename(name)
327 if name[-4:] == '.zip':
331 def is_really_module(name):
332 manifest_name = opj(dir, name, MANIFEST)
333 zipfile_name = opj(dir, name)
334 return os.path.isfile(manifest_name)
335 return map(clean, filter(is_really_module, os.listdir(dir)))
338 initialize_sys_path()
340 plist.extend(listdir(ad))
341 return list(set(plist))
343 def get_modules_with_version():
344 modules = get_modules()
345 res = dict.fromkeys(modules, adapt_version('1.0'))
346 for module in modules:
348 info = load_information_from_description_file(module)
349 res[module] = info['version']
354 def adapt_version(version):
355 serie = release.major_version
356 if version == serie or not version.startswith(serie + '.'):
357 version = '%s.%s' % (serie, version)
360 def get_test_modules(module):
361 """ Return a list of module for the addons potentialy containing tests to
362 feed unittest2.TestLoader.loadTestsFromModule() """
363 # Try to import the module
364 module = 'openerp.addons.' + module + '.tests'
368 # If module has no `tests` sub-module, no problem.
369 if str(e) != 'No module named tests':
370 _logger.exception('Can not `import %s`.', module)
373 # include submodules too
374 result = [mod_obj for name, mod_obj in sys.modules.iteritems()
375 if mod_obj # mod_obj can be None
376 if name.startswith(module)
377 if re.search(r'test_\w+$', name)]
380 # Use a custom stream object to log the test executions.
381 class TestStream(object):
382 def __init__(self, logger_name='openerp.tests'):
383 self.logger = logging.getLogger(logger_name)
384 self.r = re.compile(r'^-*$|^ *... *$|^ok$')
391 level = logging.ERROR if s.startswith(('ERROR', 'FAIL', 'Traceback')) else logging.INFO
392 for c in s.splitlines():
396 self.logger.log(level, c)
400 def runs_at(test, hook, default):
401 # by default, tests do not run post install
402 test_runs = getattr(test, hook, default)
404 # for a test suite, we're done
405 if not isinstance(test, unittest.TestCase):
408 # otherwise check the current test method to see it's been set to a
410 method = getattr(test, test._testMethodName)
411 return getattr(method, hook, test_runs)
413 runs_at_install = functools.partial(runs_at, hook='at_install', default=True)
414 runs_post_install = functools.partial(runs_at, hook='post_install', default=False)
416 def run_unit_tests(module_name, dbname, position=runs_at_install):
418 :returns: ``True`` if all of ``module_name``'s tests succeeded, ``False``
419 if any of them failed.
423 current_test = module_name
424 mods = get_test_modules(module_name)
427 tests = unwrap_suite(unittest2.TestLoader().loadTestsFromModule(m))
428 suite = unittest2.TestSuite(itertools.ifilter(position, tests))
430 if suite.countTestCases():
432 t0_sql = openerp.sql_db.sql_counter
433 _logger.info('%s running tests.', m.__name__)
434 result = unittest2.TextTestRunner(verbosity=2, stream=TestStream(m.__name__)).run(suite)
435 if time.time() - t0 > 5:
436 _logger.log(25, "%s tested in %.2fs, %s queries", m.__name__, time.time() - t0, openerp.sql_db.sql_counter - t0_sql)
437 if not result.wasSuccessful():
439 _logger.error("Module %s: %d failures, %d errors", module_name, len(result.failures), len(result.errors))
444 def unwrap_suite(test):
446 Attempts to unpack testsuites (holding suites or cases) in order to
447 generate a single stream of terminals (either test cases or customized
448 test suites). These can then be checked for run/skip attributes
451 An alternative would be to use a variant of @unittest2.skipIf with a state
452 flag of some sort e.g. @unittest2.skipIf(common.runstate != 'at_install'),
453 but then things become weird with post_install as tests should *not* run
456 if isinstance(test, unittest.TestCase):
460 subtests = list(test)
461 # custom test suite (no test cases)
462 if not len(subtests):
466 for item in itertools.chain.from_iterable(
467 itertools.imap(unwrap_suite, subtests)):
470 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: