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_resource_path(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 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 # backwards compatibility
179 get_module_resource = get_resource_path
181 def get_resource_from_path(path):
182 """Tries to extract the module name and the resource's relative path
183 out of an absolute resource path.
185 If operation is successfull, returns a tuple containing the module name, the relative path
186 to the resource using '/' as filesystem seperator[1] and the same relative path using
187 os.path.sep seperators.
189 [1] same convention as the resource path declaration in manifests
191 :param path: absolute resource path
194 :return: tuple(module_name, relative_path, os_relative_path) if possible, else None
196 resource = [path.replace(adpath, '') for adpath in ad_paths if path.startswith(adpath)]
198 relative = resource[0].split(os.path.sep)
201 module = relative.pop(0)
202 return (module, '/'.join(relative), os.path.sep.join(relative))
205 def get_module_icon(module):
206 iconpath = ['static', 'description', 'icon.png']
207 if get_module_resource(module, *iconpath):
208 return ('/' + module + '/') + '/'.join(iconpath)
209 return '/base/' + '/'.join(iconpath)
211 def get_module_root(path):
213 Get closest module's root begining from path
216 # /foo/bar/module_dir/static/src/...
218 get_module_root('/foo/bar/module_dir/static/')
219 # returns '/foo/bar/module_dir'
221 get_module_root('/foo/bar/module_dir/')
222 # returns '/foo/bar/module_dir'
224 get_module_root('/foo/bar')
227 @param path: Path from which the lookup should start
229 @return: Module root path or None if not found
231 while not os.path.exists(os.path.join(path, MANIFEST)):
232 new_path = os.path.abspath(os.path.join(path, os.pardir))
238 def load_information_from_description_file(module, mod_path=None):
240 :param module: The name of the module (sale, purchase, ...)
241 :param mod_path: Physical path of module, if not providedThe name of the module (sale, purchase, ...)
245 mod_path = get_module_path(module)
246 terp_file = mod_path and opj(mod_path, MANIFEST) or False
249 if os.path.isfile(terp_file):
250 # default values for descriptor
252 'application': False,
254 'auto_install': False,
255 'category': 'Uncategorized',
258 'icon': get_module_icon(module),
268 info.update(itertools.izip(
269 'depends data demo test init_xml update_xml demo_xml'.split(),
272 f = tools.file_open(terp_file)
274 info.update(eval(f.read()))
278 if not info.get('description'):
279 readme_path = [opj(mod_path, x) for x in README
280 if os.path.isfile(opj(mod_path, x))]
282 readme_text = tools.file_open(readme_path[0]).read()
283 info['description'] = readme_text
286 # 'active' has been renamed 'auto_install'
287 info['auto_install'] = info['active']
289 info['version'] = adapt_version(info['version'])
292 #TODO: refactor the logger in this file to follow the logging guidelines
294 _logger.debug('module %s: no %s file found.', module, MANIFEST)
297 def init_module_models(cr, module_name, obj_list):
298 """ Initialize a list of models.
300 Call _auto_init and init on each model to create or update the
301 database tables supporting the models.
303 TODO better explanation of _auto_init and init.
306 _logger.info('module %s: creating or updating database tables', module_name)
309 result = obj._auto_init(cr, {'module': module_name})
312 if hasattr(obj, 'init'):
316 obj._auto_end(cr, {'module': module_name})
318 todo.sort(key=lambda x: x[0])
323 def load_openerp_module(module_name):
324 """ Load an OpenERP module, if not already loaded.
326 This loads the module and register all of its models, thanks to either
327 the MetaModel metaclass, or the explicit instantiation of the model.
328 This is also used to load server-wide module (i.e. it is also used
329 when there is no model to register).
332 if module_name in loaded:
335 initialize_sys_path()
337 mod_path = get_module_path(module_name)
338 __import__('openerp.addons.' + module_name)
340 # Call the module's post-load hook. This can done before any model or
341 # data has been initialized. This is ok as the post-load hook is for
342 # server-wide (instead of registry-specific) functionalities.
343 info = load_information_from_description_file(module_name)
344 if info['post_load']:
345 getattr(sys.modules['openerp.addons.' + module_name], info['post_load'])()
348 msg = "Couldn't load module %s" % (module_name)
349 _logger.critical(msg)
353 loaded.append(module_name)
356 """Returns the list of module names
360 name = os.path.basename(name)
361 if name[-4:] == '.zip':
365 def is_really_module(name):
366 manifest_name = opj(dir, name, MANIFEST)
367 zipfile_name = opj(dir, name)
368 return os.path.isfile(manifest_name)
369 return map(clean, filter(is_really_module, os.listdir(dir)))
372 initialize_sys_path()
374 plist.extend(listdir(ad))
375 return list(set(plist))
377 def get_modules_with_version():
378 modules = get_modules()
379 res = dict.fromkeys(modules, adapt_version('1.0'))
380 for module in modules:
382 info = load_information_from_description_file(module)
383 res[module] = info['version']
388 def adapt_version(version):
389 serie = release.major_version
390 if version == serie or not version.startswith(serie + '.'):
391 version = '%s.%s' % (serie, version)
394 def get_test_modules(module):
395 """ Return a list of module for the addons potentialy containing tests to
396 feed unittest2.TestLoader.loadTestsFromModule() """
397 # Try to import the module
398 module = 'openerp.addons.' + module + '.tests'
402 # If module has no `tests` sub-module, no problem.
403 if str(e) != 'No module named tests':
404 _logger.exception('Can not `import %s`.', module)
407 # include submodules too
408 result = [mod_obj for name, mod_obj in sys.modules.iteritems()
409 if mod_obj # mod_obj can be None
410 if name.startswith(module)
411 if re.search(r'test_\w+$', name)]
414 # Use a custom stream object to log the test executions.
415 class TestStream(object):
416 def __init__(self, logger_name='openerp.tests'):
417 self.logger = logging.getLogger(logger_name)
418 self.r = re.compile(r'^-*$|^ *... *$|^ok$')
425 level = logging.ERROR if s.startswith(('ERROR', 'FAIL', 'Traceback')) else logging.INFO
426 for c in s.splitlines():
430 self.logger.log(level, c)
434 def runs_at(test, hook, default):
435 # by default, tests do not run post install
436 test_runs = getattr(test, hook, default)
438 # for a test suite, we're done
439 if not isinstance(test, unittest.TestCase):
442 # otherwise check the current test method to see it's been set to a
444 method = getattr(test, test._testMethodName)
445 return getattr(method, hook, test_runs)
447 runs_at_install = functools.partial(runs_at, hook='at_install', default=True)
448 runs_post_install = functools.partial(runs_at, hook='post_install', default=False)
450 def run_unit_tests(module_name, dbname, position=runs_at_install):
452 :returns: ``True`` if all of ``module_name``'s tests succeeded, ``False``
453 if any of them failed.
457 current_test = module_name
458 mods = get_test_modules(module_name)
461 tests = unwrap_suite(unittest2.TestLoader().loadTestsFromModule(m))
462 suite = unittest2.TestSuite(itertools.ifilter(position, tests))
464 if suite.countTestCases():
466 t0_sql = openerp.sql_db.sql_counter
467 _logger.info('%s running tests.', m.__name__)
468 result = unittest2.TextTestRunner(verbosity=2, stream=TestStream(m.__name__)).run(suite)
469 if time.time() - t0 > 5:
470 _logger.log(25, "%s tested in %.2fs, %s queries", m.__name__, time.time() - t0, openerp.sql_db.sql_counter - t0_sql)
471 if not result.wasSuccessful():
473 _logger.error("Module %s: %d failures, %d errors", module_name, len(result.failures), len(result.errors))
478 def unwrap_suite(test):
480 Attempts to unpack testsuites (holding suites or cases) in order to
481 generate a single stream of terminals (either test cases or customized
482 test suites). These can then be checked for run/skip attributes
485 An alternative would be to use a variant of @unittest2.skipIf with a state
486 flag of some sort e.g. @unittest2.skipIf(common.runstate != 'at_install'),
487 but then things become weird with post_install as tests should *not* run
490 if isinstance(test, unittest.TestCase):
494 subtests = list(test)
495 # custom test suite (no test cases)
496 if not len(subtests):
500 for item in itertools.chain.from_iterable(
501 itertools.imap(unwrap_suite, subtests)):
504 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: