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 ##############################################################################
26 from os.path import join as opj
31 import openerp.tools as tools
32 import openerp.tools.osutil as osutil
33 from openerp.tools.safe_eval import safe_eval as eval
36 import openerp.release as release
40 from zipfile import PyZipFile, ZIP_DEFLATED
41 from cStringIO import StringIO
45 _logger = logging.getLogger(__name__)
46 _test_logger = logging.getLogger('openerp.tests')
50 # Modules already loaded
53 _logger = logging.getLogger(__name__)
55 class AddonsImportHook(object):
57 Import hook to load OpenERP addons from multiple paths.
59 OpenERP implements its own import-hook to load its addons. OpenERP
60 addons are Python modules. Originally, they were each living in their
61 own top-level namespace, e.g. the sale module, or the hr module. For
62 backward compatibility, `import <module>` is still supported. Now they
63 are living in `openerp.addons`. The good way to import such modules is
64 thus `import openerp.addons.module`.
67 def find_module(self, module_name, package_path):
68 module_parts = module_name.split('.')
69 if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
70 return self # We act as a loader too.
72 def load_module(self, module_name):
74 module_parts = module_name.split('.')
75 if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
76 module_part = module_parts[2]
77 if module_name in sys.modules:
78 return sys.modules[module_name]
80 # Note: we don't support circular import.
81 f, path, descr = imp.find_module(module_part, ad_paths)
82 mod = imp.load_module('openerp.addons.' + module_part, f, path, descr)
83 sys.modules['openerp.addons.' + module_part] = mod
86 def initialize_sys_path():
88 Setup an import-hook to be able to import OpenERP addons from the different
91 This ensures something like ``import crm`` (or even
92 ``import openerp.addons.crm``) works even if the addons are not in the
99 ad_paths = [tools.config.addons_data_dir]
100 ad_paths += map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(','))
102 # add base module path
103 base_path = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons'))
104 ad_paths += [base_path]
106 sys.meta_path.append(AddonsImportHook())
108 def get_module_path(module, downloaded=False, display_warning=True):
109 """Return the path of the given module.
111 Search the addons paths and return the first path where the given
112 module is found. If downloaded is True, return the default addons
113 path if nothing else is found.
116 initialize_sys_path()
118 if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
119 return opj(adp, module)
122 return opj(tools.config.addons_data_dir, module)
124 _logger.warning('module %s: module not found', module)
128 def get_module_filetree(module, dir='.'):
129 path = get_module_path(module)
133 dir = os.path.normpath(dir)
136 if dir.startswith('..') or (dir and dir[0] == '/'):
137 raise Exception('Cannot access file outside the module')
139 if not os.path.isdir(path):
141 zip = zipfile.ZipFile(path + ".zip")
142 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
144 files = osutil.listdir(path, True)
148 if not f.startswith(dir):
152 f = f[len(dir)+int(not dir.endswith('/')):]
153 lst = f.split(os.sep)
156 current = current.setdefault(lst.pop(0), {})
157 current[lst.pop(0)] = None
161 def zip_directory(directory, b64enc=True, src=True):
162 """Compress a directory
164 @param directory: The directory to compress
165 @param base64enc: if True the function will encode the zip file with base64
166 @param src: Integrate the source files
168 @return: a string containing the zip file
171 RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
173 def _zippy(archive, path, src=True):
174 path = os.path.abspath(path)
175 base = os.path.basename(path)
176 for f in osutil.listdir(path, True):
177 bf = os.path.basename(f)
178 if not RE_exclude.search(bf) and (src or bf == '__openerp__.py' or not bf.endswith('.py')):
179 archive.write(os.path.join(path, f), os.path.join(base, f))
181 archname = StringIO()
182 archive = PyZipFile(archname, "w", ZIP_DEFLATED)
184 # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8)
185 directory = tools.ustr(directory).encode('utf-8')
187 archive.writepy(directory)
188 _zippy(archive, directory, src=src)
190 archive_data = archname.getvalue()
194 return base64.encodestring(archive_data)
198 def get_module_as_zip(modulename, b64enc=True, src=True):
199 """Generate a module as zip file with the source or not and can do a base64 encoding
201 @param modulename: The module name
202 @param b64enc: if True the function will encode the zip file with base64
203 @param src: Integrate the source files
205 @return: a stream to store in a file-like object
208 ap = get_module_path(str(modulename))
210 raise Exception('Unable to find path for module %s' % modulename)
212 ap = ap.encode('utf8')
213 if os.path.isfile(ap + '.zip'):
214 val = file(ap + '.zip', 'rb').read()
216 val = base64.encodestring(val)
218 val = zip_directory(ap, b64enc, src)
223 def get_module_resource(module, *args):
224 """Return the full path of a resource of the given module.
226 :param module: module name
227 :param list(str) args: resource path components within module
230 :return: absolute path to the resource
232 TODO name it get_resource_path
233 TODO make it available inside on osv object (self.get_resource_path)
235 mod_path = get_module_path(module)
236 if not mod_path: return False
237 resource_path = opj(mod_path, *args)
238 if os.path.isdir(mod_path):
239 # the module is a directory - ignore zip behavior
240 if os.path.exists(resource_path):
242 elif zipfile.is_zipfile(mod_path + '.zip'):
243 zip = zipfile.ZipFile( mod_path + ".zip")
244 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
245 resource_path = '/'.join(args)
246 if resource_path in files:
247 return opj(mod_path, resource_path)
250 def get_module_icon(module):
251 iconpath = ['static', 'description', 'icon.png']
252 if get_module_resource(module, *iconpath):
253 return ('/' + module + '/') + '/'.join(iconpath)
254 return '/base/' + '/'.join(iconpath)
256 def load_information_from_description_file(module):
258 :param module: The name of the module (sale, purchase, ...)
261 terp_file = get_module_resource(module, '__openerp__.py')
262 mod_path = get_module_path(module)
265 if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
266 # default values for descriptor
268 'application': False,
270 'auto_install': False,
271 'category': 'Uncategorized',
274 'icon': get_module_icon(module),
285 info.update(itertools.izip(
286 'depends data demo test init_xml update_xml demo_xml'.split(),
289 f = tools.file_open(terp_file)
291 info.update(eval(f.read()))
296 # 'active' has been renamed 'auto_install'
297 info['auto_install'] = info['active']
299 info['version'] = adapt_version(info['version'])
302 #TODO: refactor the logger in this file to follow the logging guidelines
304 _logger.debug('module %s: no __openerp__.py file found.', module)
308 def init_module_models(cr, module_name, obj_list):
309 """ Initialize a list of models.
311 Call _auto_init and init on each model to create or update the
312 database tables supporting the models.
314 TODO better explanation of _auto_init and init.
317 _logger.info('module %s: creating or updating database tables', module_name)
320 result = obj._auto_init(cr, {'module': module_name})
323 if hasattr(obj, 'init'):
327 obj._auto_end(cr, {'module': module_name})
334 def load_openerp_module(module_name):
335 """ Load an OpenERP module, if not already loaded.
337 This loads the module and register all of its models, thanks to either
338 the MetaModel metaclass, or the explicit instantiation of the model.
339 This is also used to load server-wide module (i.e. it is also used
340 when there is no model to register).
343 if module_name in loaded:
346 initialize_sys_path()
348 mod_path = get_module_path(module_name)
349 zip_mod_path = '' if not mod_path else mod_path + '.zip'
350 if not os.path.isfile(zip_mod_path):
351 __import__('openerp.addons.' + module_name)
353 zimp = zipimport.zipimporter(zip_mod_path)
354 zimp.load_module(module_name)
356 # Call the module's post-load hook. This can done before any model or
357 # data has been initialized. This is ok as the post-load hook is for
358 # server-wide (instead of registry-specific) functionalities.
359 info = load_information_from_description_file(module_name)
360 if info['post_load']:
361 getattr(sys.modules['openerp.addons.' + module_name], info['post_load'])()
364 mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
365 msg = "Couldn't load %smodule %s" % (mt, module_name)
366 _logger.critical(msg)
370 loaded.append(module_name)
373 """Returns the list of module names
377 name = os.path.basename(name)
378 if name[-4:] == '.zip':
382 def is_really_module(name):
383 manifest_name = opj(dir, name, '__openerp__.py')
384 zipfile_name = opj(dir, name)
385 return os.path.isfile(manifest_name) or zipfile.is_zipfile(zipfile_name)
386 return map(clean, filter(is_really_module, os.listdir(dir)))
389 initialize_sys_path()
391 plist.extend(listdir(ad))
392 return list(set(plist))
395 def get_modules_with_version():
396 modules = get_modules()
397 res = dict.fromkeys(modules, adapt_version('1.0'))
398 for module in modules:
400 info = load_information_from_description_file(module)
401 res[module] = info['version']
406 def adapt_version(version):
407 serie = release.major_version
408 if version == serie or not version.startswith(serie + '.'):
409 version = '%s.%s' % (serie, version)
413 def get_test_modules(module, submodule, explode):
415 Return a list of submodules containing tests.
418 - the name of a submodule
420 - '__sanity_checks__'
422 # Turn command-line module, submodule into importable names.
425 elif module == 'openerp':
426 module = 'openerp.tests'
428 module = 'openerp.addons.' + module + '.tests'
430 # Try to import the module
435 print 'Can not `import %s`.' % module
437 logging.exception('')
440 if str(e) == 'No module named tests':
441 # It seems the module has no `tests` sub-module, no problem.
444 _logger.exception('Can not `import %s`.', module)
447 # Discover available test sub-modules.
448 m = sys.modules[module]
449 submodule_names = sorted([x for x in dir(m) \
450 if x.startswith('test_') and \
451 isinstance(getattr(m, x), types.ModuleType)])
452 submodules = [getattr(m, x) for x in submodule_names]
454 def show_submodules_and_exit():
456 print 'Available submodules are:'
457 for x in submodule_names:
461 if submodule is None:
462 # Use auto-discovered sub-modules.
464 elif submodule == '__fast_suite__':
465 # Obtain the explicit test sub-modules list.
466 ms = getattr(sys.modules[module], 'fast_suite', None)
467 # `suite` was used before the 6.1 release instead of `fast_suite`.
468 ms = ms if ms else getattr(sys.modules[module], 'suite', None)
471 print 'The module `%s` has no defined test suite.' % (module,)
472 show_submodules_and_exit()
475 elif submodule == '__sanity_checks__':
476 ms = getattr(sys.modules[module], 'checks', None)
479 print 'The module `%s` has no defined sanity checks.' % (module,)
480 show_submodules_and_exit()
484 # Pick the command-line-specified test sub-module.
485 m = getattr(sys.modules[module], submodule, None)
490 print 'The module `%s` has no submodule named `%s`.' % \
492 show_submodules_and_exit()
498 def run_unit_tests(module_name):
500 Return True or False if some tests were found and succeeded or failed.
501 Return None if no test was found.
504 ms = get_test_modules(module_name, '__fast_suite__', explode=False)
505 # TODO: No need to try again if the above call failed because of e.g. a syntax error.
506 ms.extend(get_test_modules(module_name, '__sanity_checks__', explode=False))
507 suite = unittest2.TestSuite()
509 suite.addTests(unittest2.TestLoader().loadTestsFromModule(m))
511 _test_logger.info('module %s: executing %s `fast_suite` and/or `checks` sub-modules', module_name, len(ms))
512 # Use a custom stream object to log the test executions.
513 class MyStream(object):
515 self.r = re.compile(r'^-*$|^ *... *$|^ok$')
522 for c in s.split('\n'):
527 result = unittest2.TextTestRunner(verbosity=2, stream=MyStream()).run(suite)
528 if result.wasSuccessful():
531 _logger.error('module %s: at least one error occurred in a test', module_name)
534 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: