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__)
47 _ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base)
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`.
66 For backward compatibility, loading an addons puts it in `sys.modules`
67 under both the legacy (short) name, and the new (longer) name. This
70 import openerp.addons.hr
71 loads the hr addons only once.
73 When an OpenERP addons name clashes with some other installed Python
74 module (for instance this is the case of the `resource` addons),
75 obtaining the OpenERP addons is only possible with the long name. The
76 short name will give the expected Python module.
78 Instead of relying on some addons path, an alternative approach would be
79 to use pkg_resources entry points from already installed Python libraries
80 (and install our addons as such). Even when implemented, we would still
81 have to support the addons path approach for backward compatibility.
84 def find_module(self, module_name, package_path):
85 module_parts = module_name.split('.')
86 if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
87 return self # We act as a loader too.
89 # TODO list of loadable modules can be cached instead of always
90 # calling get_module_path().
91 if len(module_parts) == 1 and \
92 get_module_path(module_parts[0],
93 display_warning=False):
95 # Check if the bare module name clashes with another module.
96 f, path, descr = imp.find_module(module_parts[0])
98 Ambiguous import: the OpenERP module `%s` is shadowed by another
99 module (available at %s).
100 To import it, use `import openerp.addons.<module>.`.""" % (module_name, path))
102 except ImportError, e:
103 # Using `import <module_name>` instead of
104 # `import openerp.addons.<module_name>` is ugly but not harmful
105 # and kept for backward compatibility.
106 return self # We act as a loader too.
108 def load_module(self, module_name):
110 module_parts = module_name.split('.')
111 if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
112 module_part = module_parts[2]
113 if module_name in sys.modules:
114 return sys.modules[module_name]
116 if len(module_parts) == 1:
117 module_part = module_parts[0]
118 if module_part in sys.modules:
119 return sys.modules[module_part]
122 # Check if the bare module name shadows another module.
123 f, path, descr = imp.find_module(module_part)
125 except ImportError, e:
126 # Using `import <module_name>` instead of
127 # `import openerp.addons.<module_name>` is ugly but not harmful
128 # and kept for backward compatibility.
131 # Note: we don't support circular import.
132 f, path, descr = imp.find_module(module_part, ad_paths)
133 mod = imp.load_module('openerp.addons.' + module_part, f, path, descr)
135 sys.modules[module_part] = mod
136 for k in sys.modules.keys():
137 if k.startswith('openerp.addons.' + module_part):
138 sys.modules[k[len('openerp.addons.'):]] = sys.modules[k]
139 sys.modules['openerp.addons.' + module_part] = mod
142 def initialize_sys_path():
144 Setup an import-hook to be able to import OpenERP addons from the different
147 This ensures something like ``import crm`` (or even
148 ``import openerp.addons.crm``) works even if the addons are not in the
155 ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(','))
156 ad_paths.append(os.path.abspath(_ad)) # for get_module_path
157 sys.meta_path.append(AddonsImportHook())
159 def get_module_path(module, downloaded=False, display_warning=True):
160 """Return the path of the given module.
162 Search the addons paths and return the first path where the given
163 module is found. If downloaded is True, return the default addons
164 path if nothing else is found.
167 initialize_sys_path()
169 if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
170 return opj(adp, module)
173 return opj(_ad, module)
175 _logger.warning('module %s: module not found', module)
179 def get_module_filetree(module, dir='.'):
180 path = get_module_path(module)
184 dir = os.path.normpath(dir)
187 if dir.startswith('..') or (dir and dir[0] == '/'):
188 raise Exception('Cannot access file outside the module')
190 if not os.path.isdir(path):
192 zip = zipfile.ZipFile(path + ".zip")
193 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
195 files = osutil.listdir(path, True)
199 if not f.startswith(dir):
203 f = f[len(dir)+int(not dir.endswith('/')):]
204 lst = f.split(os.sep)
207 current = current.setdefault(lst.pop(0), {})
208 current[lst.pop(0)] = None
212 def zip_directory(directory, b64enc=True, src=True):
213 """Compress a directory
215 @param directory: The directory to compress
216 @param base64enc: if True the function will encode the zip file with base64
217 @param src: Integrate the source files
219 @return: a string containing the zip file
222 RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
224 def _zippy(archive, path, src=True):
225 path = os.path.abspath(path)
226 base = os.path.basename(path)
227 for f in osutil.listdir(path, True):
228 bf = os.path.basename(f)
229 if not RE_exclude.search(bf) and (src or bf in ('__openerp__.py', '__terp__.py') or not bf.endswith('.py')):
230 archive.write(os.path.join(path, f), os.path.join(base, f))
232 archname = StringIO()
233 archive = PyZipFile(archname, "w", ZIP_DEFLATED)
235 # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8)
236 directory = tools.ustr(directory).encode('utf-8')
238 archive.writepy(directory)
239 _zippy(archive, directory, src=src)
241 archive_data = archname.getvalue()
245 return base64.encodestring(archive_data)
249 def get_module_as_zip(modulename, b64enc=True, src=True):
250 """Generate a module as zip file with the source or not and can do a base64 encoding
252 @param modulename: The module name
253 @param b64enc: if True the function will encode the zip file with base64
254 @param src: Integrate the source files
256 @return: a stream to store in a file-like object
259 ap = get_module_path(str(modulename))
261 raise Exception('Unable to find path for module %s' % modulename)
263 ap = ap.encode('utf8')
264 if os.path.isfile(ap + '.zip'):
265 val = file(ap + '.zip', 'rb').read()
267 val = base64.encodestring(val)
269 val = zip_directory(ap, b64enc, src)
274 def get_module_resource(module, *args):
275 """Return the full path of a resource of the given module.
277 :param module: module name
278 :param list(str) args: resource path components within module
281 :return: absolute path to the resource
283 TODO name it get_resource_path
284 TODO make it available inside on osv object (self.get_resource_path)
286 mod_path = get_module_path(module)
287 if not mod_path: return False
288 resource_path = opj(mod_path, *args)
289 if os.path.isdir(mod_path):
290 # the module is a directory - ignore zip behavior
291 if os.path.exists(resource_path):
293 elif zipfile.is_zipfile(mod_path + '.zip'):
294 zip = zipfile.ZipFile( mod_path + ".zip")
295 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
296 resource_path = '/'.join(args)
297 if resource_path in files:
298 return opj(mod_path, resource_path)
301 def get_module_icon(module):
302 iconpath = ['static', 'src', 'img', 'icon.png']
303 if get_module_resource(module, *iconpath):
304 return ('/' + module + '/') + '/'.join(iconpath)
305 return '/base/' + '/'.join(iconpath)
307 def load_information_from_description_file(module):
309 :param module: The name of the module (sale, purchase, ...)
312 terp_file = get_module_resource(module, '__openerp__.py')
314 terp_file = get_module_resource(module, '__terp__.py')
315 mod_path = get_module_path(module)
318 if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
319 # default values for descriptor
321 'application': False,
323 'auto_install': False,
324 'category': 'Uncategorized',
327 'icon': get_module_icon(module),
338 info.update(itertools.izip(
339 'depends data demo test init_xml update_xml demo_xml'.split(),
342 f = tools.file_open(terp_file)
344 info.update(eval(f.read()))
349 # 'active' has been renamed 'auto_install'
350 info['auto_install'] = info['active']
352 info['version'] = adapt_version(info['version'])
355 #TODO: refactor the logger in this file to follow the logging guidelines
357 _logger.debug('module %s: no descriptor file'
358 ' found: __openerp__.py or __terp__.py (deprecated)', module)
362 def init_module_models(cr, module_name, obj_list):
363 """ Initialize a list of models.
365 Call _auto_init and init on each model to create or update the
366 database tables supporting the models.
368 TODO better explanation of _auto_init and init.
371 _logger.info('module %s: creating or updating database tables', module_name)
374 result = obj._auto_init(cr, {'module': module_name})
377 if hasattr(obj, 'init'):
381 obj._auto_end(cr, {'module': module_name})
388 def load_openerp_module(module_name):
389 """ Load an OpenERP module, if not already loaded.
391 This loads the module and register all of its models, thanks to either
392 the MetaModel metaclass, or the explicit instantiation of the model.
393 This is also used to load server-wide module (i.e. it is also used
394 when there is no model to register).
397 if module_name in loaded:
400 initialize_sys_path()
402 mod_path = get_module_path(module_name)
403 zip_mod_path = '' if not mod_path else mod_path + '.zip'
404 if not os.path.isfile(zip_mod_path):
405 __import__('openerp.addons.' + module_name)
407 zimp = zipimport.zipimporter(zip_mod_path)
408 zimp.load_module(module_name)
410 # Call the module's post-load hook. This can done before any model or
411 # data has been initialized. This is ok as the post-load hook is for
412 # server-wide (instead of registry-specific) functionalities.
413 info = load_information_from_description_file(module_name)
414 if info['post_load']:
415 getattr(sys.modules['openerp.addons.' + module_name], info['post_load'])()
418 mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
419 msg = "Couldn't load %smodule %s" % (mt, module_name)
420 _logger.critical(msg)
424 loaded.append(module_name)
427 """Returns the list of module names
431 name = os.path.basename(name)
432 if name[-4:] == '.zip':
436 def is_really_module(name):
437 manifest_name = opj(dir, name, '__openerp__.py')
438 zipfile_name = opj(dir, name)
439 return os.path.isfile(manifest_name) or zipfile.is_zipfile(zipfile_name)
440 return map(clean, filter(is_really_module, os.listdir(dir)))
443 initialize_sys_path()
445 plist.extend(listdir(ad))
446 return list(set(plist))
449 def get_modules_with_version():
450 modules = get_modules()
451 res = dict.fromkeys(modules, adapt_version('1.0'))
452 for module in modules:
454 info = load_information_from_description_file(module)
455 res[module] = info['version']
460 def adapt_version(version):
461 serie = release.major_version
462 if version == serie or not version.startswith(serie + '.'):
463 version = '%s.%s' % (serie, version)
467 def get_test_modules(module, submodule, explode):
469 Return a list of submodules containing tests.
472 - the name of a submodule
474 - '__sanity_checks__'
476 # Turn command-line module, submodule into importable names.
479 elif module == 'openerp':
480 module = 'openerp.tests'
482 module = 'openerp.addons.' + module + '.tests'
484 # Try to import the module
489 print 'Can not `import %s`.' % module
491 logging.exception('')
494 if str(e) == 'No module named tests':
495 # It seems the module has no `tests` sub-module, no problem.
498 _logger.exception('Can not `import %s`.', module)
501 # Discover available test sub-modules.
502 m = sys.modules[module]
503 submodule_names = sorted([x for x in dir(m) \
504 if x.startswith('test_') and \
505 isinstance(getattr(m, x), types.ModuleType)])
506 submodules = [getattr(m, x) for x in submodule_names]
508 def show_submodules_and_exit():
510 print 'Available submodules are:'
511 for x in submodule_names:
515 if submodule is None:
516 # Use auto-discovered sub-modules.
518 elif submodule == '__fast_suite__':
519 # Obtain the explicit test sub-modules list.
520 ms = getattr(sys.modules[module], 'fast_suite', None)
521 # `suite` was used before the 6.1 release instead of `fast_suite`.
522 ms = ms if ms else getattr(sys.modules[module], 'suite', None)
525 print 'The module `%s` has no defined test suite.' % (module,)
526 show_submodules_and_exit()
529 elif submodule == '__sanity_checks__':
530 ms = getattr(sys.modules[module], 'checks', None)
533 print 'The module `%s` has no defined sanity checks.' % (module,)
534 show_submodules_and_exit()
538 # Pick the command-line-specified test sub-module.
539 m = getattr(sys.modules[module], submodule, None)
544 print 'The module `%s` has no submodule named `%s`.' % \
546 show_submodules_and_exit()
552 def run_unit_tests(module_name):
554 Return True or False if some tests were found and succeeded or failed.
555 Return None if no test was found.
558 ms = get_test_modules(module_name, '__fast_suite__', explode=False)
559 # TODO: No need to try again if the above call failed because of e.g. a syntax error.
560 ms.extend(get_test_modules(module_name, '__sanity_checks__', explode=False))
561 suite = unittest2.TestSuite()
563 suite.addTests(unittest2.TestLoader().loadTestsFromModule(m))
565 _logger.log(logging.TEST, 'module %s: executing %s `fast_suite` and/or `checks` sub-modules', module_name, len(ms))
566 # Use a custom stream object to log the test executions.
567 class MyStream(object):
569 self.r = re.compile(r'^-*$|^ *... *$|^ok$')
576 for c in s.split('\n'):
580 _logger.log(logging.TEST, c)
581 result = unittest2.TextTestRunner(verbosity=2, stream=MyStream()).run(suite)
582 if result.wasSuccessful():
585 _logger.error('module %s: at least one error occurred in a test', module_name)
588 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: