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-2012 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')
48 _ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base)
51 # Modules already loaded
54 _logger = logging.getLogger(__name__)
56 class AddonsImportHook(object):
58 Import hook to load OpenERP addons from multiple paths.
60 OpenERP implements its own import-hook to load its addons. OpenERP
61 addons are Python modules. Originally, they were each living in their
62 own top-level namespace, e.g. the sale module, or the hr module. For
63 backward compatibility, `import <module>` is still supported. Now they
64 are living in `openerp.addons`. The good way to import such modules is
65 thus `import openerp.addons.module`.
68 def find_module(self, module_name, package_path):
69 module_parts = module_name.split('.')
70 if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
71 return self # We act as a loader too.
73 def load_module(self, module_name):
75 module_parts = module_name.split('.')
76 if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
77 module_part = module_parts[2]
78 if module_name in sys.modules:
79 return sys.modules[module_name]
81 # Note: we don't support circular import.
82 f, path, descr = imp.find_module(module_part, ad_paths)
83 mod = imp.load_module('openerp.addons.' + module_part, f, path, descr)
84 sys.modules['openerp.addons.' + module_part] = mod
87 def initialize_sys_path():
89 Setup an import-hook to be able to import OpenERP addons from the different
92 This ensures something like ``import crm`` (or even
93 ``import openerp.addons.crm``) works even if the addons are not in the
100 ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(','))
101 ad_paths.append(os.path.abspath(_ad)) # for get_module_path
102 sys.meta_path.append(AddonsImportHook())
104 def get_module_path(module, downloaded=False, display_warning=True):
105 """Return the path of the given module.
107 Search the addons paths and return the first path where the given
108 module is found. If downloaded is True, return the default addons
109 path if nothing else is found.
112 initialize_sys_path()
114 if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
115 return opj(adp, module)
118 return opj(_ad, module)
120 _logger.warning('module %s: module not found', module)
124 def get_module_filetree(module, dir='.'):
125 path = get_module_path(module)
129 dir = os.path.normpath(dir)
132 if dir.startswith('..') or (dir and dir[0] == '/'):
133 raise Exception('Cannot access file outside the module')
135 if not os.path.isdir(path):
137 zip = zipfile.ZipFile(path + ".zip")
138 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
140 files = 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 zip_directory(directory, b64enc=True, src=True):
158 """Compress a directory
160 @param directory: The directory to compress
161 @param base64enc: if True the function will encode the zip file with base64
162 @param src: Integrate the source files
164 @return: a string containing the zip file
167 RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
169 def _zippy(archive, path, src=True):
170 path = os.path.abspath(path)
171 base = os.path.basename(path)
172 for f in osutil.listdir(path, True):
173 bf = os.path.basename(f)
174 if not RE_exclude.search(bf) and (src or bf == '__openerp__.py' or not bf.endswith('.py')):
175 archive.write(os.path.join(path, f), os.path.join(base, f))
177 archname = StringIO()
178 archive = PyZipFile(archname, "w", ZIP_DEFLATED)
180 # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8)
181 directory = tools.ustr(directory).encode('utf-8')
183 archive.writepy(directory)
184 _zippy(archive, directory, src=src)
186 archive_data = archname.getvalue()
190 return base64.encodestring(archive_data)
194 def get_module_as_zip(modulename, b64enc=True, src=True):
195 """Generate a module as zip file with the source or not and can do a base64 encoding
197 @param modulename: The module name
198 @param b64enc: if True the function will encode the zip file with base64
199 @param src: Integrate the source files
201 @return: a stream to store in a file-like object
204 ap = get_module_path(str(modulename))
206 raise Exception('Unable to find path for module %s' % modulename)
208 ap = ap.encode('utf8')
209 if os.path.isfile(ap + '.zip'):
210 val = file(ap + '.zip', 'rb').read()
212 val = base64.encodestring(val)
214 val = zip_directory(ap, b64enc, src)
219 def get_module_resource(module, *args):
220 """Return the full path of a resource of the given module.
222 :param module: module name
223 :param list(str) args: resource path components within module
226 :return: absolute path to the resource
228 TODO name it get_resource_path
229 TODO make it available inside on osv object (self.get_resource_path)
231 mod_path = get_module_path(module)
232 if not mod_path: return False
233 resource_path = opj(mod_path, *args)
234 if os.path.isdir(mod_path):
235 # the module is a directory - ignore zip behavior
236 if os.path.exists(resource_path):
238 elif zipfile.is_zipfile(mod_path + '.zip'):
239 zip = zipfile.ZipFile( mod_path + ".zip")
240 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
241 resource_path = '/'.join(args)
242 if resource_path in files:
243 return opj(mod_path, resource_path)
246 def get_module_icon(module):
247 iconpath = ['static', 'src', 'img', 'icon.png']
248 if get_module_resource(module, *iconpath):
249 return ('/' + module + '/') + '/'.join(iconpath)
250 return '/base/' + '/'.join(iconpath)
252 def load_information_from_description_file(module):
254 :param module: The name of the module (sale, purchase, ...)
257 terp_file = get_module_resource(module, '__openerp__.py')
258 mod_path = get_module_path(module)
261 if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
262 # default values for descriptor
264 'application': False,
266 'auto_install': False,
267 'category': 'Uncategorized',
270 'icon': get_module_icon(module),
281 info.update(itertools.izip(
282 'depends data demo test init_xml update_xml demo_xml'.split(),
285 f = tools.file_open(terp_file)
287 info.update(eval(f.read()))
292 # 'active' has been renamed 'auto_install'
293 info['auto_install'] = info['active']
295 info['version'] = adapt_version(info['version'])
298 #TODO: refactor the logger in this file to follow the logging guidelines
300 _logger.debug('module %s: no __openerp__.py file found.', module)
304 def init_module_models(cr, module_name, obj_list):
305 """ Initialize a list of models.
307 Call _auto_init and init on each model to create or update the
308 database tables supporting the models.
310 TODO better explanation of _auto_init and init.
313 _logger.info('module %s: creating or updating database tables', module_name)
316 result = obj._auto_init(cr, {'module': module_name})
319 if hasattr(obj, 'init'):
323 obj._auto_end(cr, {'module': module_name})
330 def load_openerp_module(module_name):
331 """ Load an OpenERP module, if not already loaded.
333 This loads the module and register all of its models, thanks to either
334 the MetaModel metaclass, or the explicit instantiation of the model.
335 This is also used to load server-wide module (i.e. it is also used
336 when there is no model to register).
339 if module_name in loaded:
342 initialize_sys_path()
344 mod_path = get_module_path(module_name)
345 zip_mod_path = '' if not mod_path else mod_path + '.zip'
346 if not os.path.isfile(zip_mod_path):
347 __import__('openerp.addons.' + module_name)
349 zimp = zipimport.zipimporter(zip_mod_path)
350 zimp.load_module(module_name)
352 # Call the module's post-load hook. This can done before any model or
353 # data has been initialized. This is ok as the post-load hook is for
354 # server-wide (instead of registry-specific) functionalities.
355 info = load_information_from_description_file(module_name)
356 if info['post_load']:
357 getattr(sys.modules['openerp.addons.' + module_name], info['post_load'])()
360 mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
361 msg = "Couldn't load %smodule %s" % (mt, module_name)
362 _logger.critical(msg)
366 loaded.append(module_name)
369 """Returns the list of module names
373 name = os.path.basename(name)
374 if name[-4:] == '.zip':
378 def is_really_module(name):
379 manifest_name = opj(dir, name, '__openerp__.py')
380 zipfile_name = opj(dir, name)
381 return os.path.isfile(manifest_name) or zipfile.is_zipfile(zipfile_name)
382 return map(clean, filter(is_really_module, os.listdir(dir)))
385 initialize_sys_path()
387 plist.extend(listdir(ad))
388 return list(set(plist))
391 def get_modules_with_version():
392 modules = get_modules()
393 res = dict.fromkeys(modules, adapt_version('1.0'))
394 for module in modules:
396 info = load_information_from_description_file(module)
397 res[module] = info['version']
402 def adapt_version(version):
403 serie = release.major_version
404 if version == serie or not version.startswith(serie + '.'):
405 version = '%s.%s' % (serie, version)
409 def get_test_modules(module, submodule, explode):
411 Return a list of submodules containing tests.
414 - the name of a submodule
416 - '__sanity_checks__'
418 # Turn command-line module, submodule into importable names.
421 elif module == 'openerp':
422 module = 'openerp.tests'
424 module = 'openerp.addons.' + module + '.tests'
426 # Try to import the module
431 print 'Can not `import %s`.' % module
433 logging.exception('')
436 if str(e) == 'No module named tests':
437 # It seems the module has no `tests` sub-module, no problem.
440 _logger.exception('Can not `import %s`.', module)
443 # Discover available test sub-modules.
444 m = sys.modules[module]
445 submodule_names = sorted([x for x in dir(m) \
446 if x.startswith('test_') and \
447 isinstance(getattr(m, x), types.ModuleType)])
448 submodules = [getattr(m, x) for x in submodule_names]
450 def show_submodules_and_exit():
452 print 'Available submodules are:'
453 for x in submodule_names:
457 if submodule is None:
458 # Use auto-discovered sub-modules.
460 elif submodule == '__fast_suite__':
461 # Obtain the explicit test sub-modules list.
462 ms = getattr(sys.modules[module], 'fast_suite', None)
463 # `suite` was used before the 6.1 release instead of `fast_suite`.
464 ms = ms if ms else getattr(sys.modules[module], 'suite', None)
467 print 'The module `%s` has no defined test suite.' % (module,)
468 show_submodules_and_exit()
471 elif submodule == '__sanity_checks__':
472 ms = getattr(sys.modules[module], 'checks', None)
475 print 'The module `%s` has no defined sanity checks.' % (module,)
476 show_submodules_and_exit()
480 # Pick the command-line-specified test sub-module.
481 m = getattr(sys.modules[module], submodule, None)
486 print 'The module `%s` has no submodule named `%s`.' % \
488 show_submodules_and_exit()
494 def run_unit_tests(module_name):
496 Return True or False if some tests were found and succeeded or failed.
497 Return None if no test was found.
500 ms = get_test_modules(module_name, '__fast_suite__', explode=False)
501 # TODO: No need to try again if the above call failed because of e.g. a syntax error.
502 ms.extend(get_test_modules(module_name, '__sanity_checks__', explode=False))
503 suite = unittest2.TestSuite()
505 suite.addTests(unittest2.TestLoader().loadTestsFromModule(m))
507 _test_logger.info('module %s: executing %s `fast_suite` and/or `checks` sub-modules', module_name, len(ms))
508 # Use a custom stream object to log the test executions.
509 class MyStream(object):
511 self.r = re.compile(r'^-*$|^ *... *$|^ok$')
518 for c in s.split('\n'):
523 result = unittest2.TextTestRunner(verbosity=2, stream=MyStream()).run(suite)
524 if result.wasSuccessful():
527 _logger.error('module %s: at least one error occurred in a test', module_name)
530 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: