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-2011 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
33 import openerp.osv as osv
34 import openerp.tools as tools
35 import openerp.tools.osutil as osutil
36 from openerp.tools.safe_eval import safe_eval as eval
37 from openerp.tools.translate import _
39 import openerp.netsvc as netsvc
42 import openerp.release as release
46 from zipfile import PyZipFile, ZIP_DEFLATED
47 from cStringIO import StringIO
51 import openerp.modules.db
52 import openerp.modules.graph
54 _logger = logging.getLogger(__name__)
56 _ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base)
59 # Modules already loaded
62 _logger = logging.getLogger(__name__)
64 class AddonsImportHook(object):
66 Import hook to load OpenERP addons from multiple paths.
68 OpenERP implements its own import-hook to load its addons. OpenERP
69 addons are Python modules. Originally, they were each living in their
70 own top-level namespace, e.g. the sale module, or the hr module. For
71 backward compatibility, `import <module>` is still supported. Now they
72 are living in `openerp.addons`. The good way to import such modules is
73 thus `import openerp.addons.module`.
75 For backward compatibility, loading an addons puts it in `sys.modules`
76 under both the legacy (short) name, and the new (longer) name. This
79 import openerp.addons.hr
80 loads the hr addons only once.
82 When an OpenERP addons name clashes with some other installed Python
83 module (for instance this is the case of the `resource` addons),
84 obtaining the OpenERP addons is only possible with the long name. The
85 short name will give the expected Python module.
87 Instead of relying on some addons path, an alternative approach would be
88 to use pkg_resources entry points from already installed Python libraries
89 (and install our addons as such). Even when implemented, we would still
90 have to support the addons path approach for backward compatibility.
93 def find_module(self, module_name, package_path):
94 module_parts = module_name.split('.')
95 if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
96 return self # We act as a loader too.
98 # TODO list of loadable modules can be cached instead of always
99 # calling get_module_path().
100 if len(module_parts) == 1 and \
101 get_module_path(module_parts[0],
102 display_warning=False):
104 # Check if the bare module name clashes with another module.
105 f, path, descr = imp.find_module(module_parts[0])
107 Ambiguous import: the OpenERP module `%s` is shadowed by another
108 module (available at %s).
109 To import it, use `import openerp.addons.<module>.`.""" % (module_name, path))
111 except ImportError, e:
112 # Using `import <module_name>` instead of
113 # `import openerp.addons.<module_name>` is ugly but not harmful
114 # and kept for backward compatibility.
115 return self # We act as a loader too.
117 def load_module(self, module_name):
119 module_parts = module_name.split('.')
120 if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
121 module_part = module_parts[2]
122 if module_name in sys.modules:
123 return sys.modules[module_name]
125 if len(module_parts) == 1:
126 module_part = module_parts[0]
127 if module_part in sys.modules:
128 return sys.modules[module_part]
131 # Check if the bare module name shadows another module.
132 f, path, descr = imp.find_module(module_part)
134 except ImportError, e:
135 # Using `import <module_name>` instead of
136 # `import openerp.addons.<module_name>` is ugly but not harmful
137 # and kept for backward compatibility.
140 # Note: we don't support circular import.
141 f, path, descr = imp.find_module(module_part, ad_paths)
142 mod = imp.load_module('openerp.addons.' + module_part, f, path, descr)
144 sys.modules[module_part] = mod
145 for k in sys.modules.keys():
146 if k.startswith('openerp.addons.' + module_part):
147 sys.modules[k[len('openerp.addons.'):]] = sys.modules[k]
148 sys.modules['openerp.addons.' + module_part] = mod
151 def initialize_sys_path():
153 Setup an import-hook to be able to import OpenERP addons from the different
156 This ensures something like ``import crm`` (or even
157 ``import openerp.addons.crm``) works even if the addons are not in the
164 ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(','))
165 ad_paths.append(_ad) # for get_module_path
166 sys.meta_path.append(AddonsImportHook())
168 def get_module_path(module, downloaded=False, display_warning=True):
169 """Return the path of the given module.
171 Search the addons paths and return the first path where the given
172 module is found. If downloaded is True, return the default addons
173 path if nothing else is found.
176 initialize_sys_path()
178 if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
179 return opj(adp, module)
182 return opj(_ad, module)
184 _logger.warning('module %s: module not found', module)
188 def get_module_filetree(module, dir='.'):
189 path = get_module_path(module)
193 dir = os.path.normpath(dir)
196 if dir.startswith('..') or (dir and dir[0] == '/'):
197 raise Exception('Cannot access file outside the module')
199 if not os.path.isdir(path):
201 zip = zipfile.ZipFile(path + ".zip")
202 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
204 files = osutil.listdir(path, True)
208 if not f.startswith(dir):
212 f = f[len(dir)+int(not dir.endswith('/')):]
213 lst = f.split(os.sep)
216 current = current.setdefault(lst.pop(0), {})
217 current[lst.pop(0)] = None
221 def zip_directory(directory, b64enc=True, src=True):
222 """Compress a directory
224 @param directory: The directory to compress
225 @param base64enc: if True the function will encode the zip file with base64
226 @param src: Integrate the source files
228 @return: a string containing the zip file
231 RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
233 def _zippy(archive, path, src=True):
234 path = os.path.abspath(path)
235 base = os.path.basename(path)
236 for f in osutil.listdir(path, True):
237 bf = os.path.basename(f)
238 if not RE_exclude.search(bf) and (src or bf in ('__openerp__.py', '__terp__.py') or not bf.endswith('.py')):
239 archive.write(os.path.join(path, f), os.path.join(base, f))
241 archname = StringIO()
242 archive = PyZipFile(archname, "w", ZIP_DEFLATED)
244 # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8)
245 directory = tools.ustr(directory).encode('utf-8')
247 archive.writepy(directory)
248 _zippy(archive, directory, src=src)
250 archive_data = archname.getvalue()
254 return base64.encodestring(archive_data)
258 def get_module_as_zip(modulename, b64enc=True, src=True):
259 """Generate a module as zip file with the source or not and can do a base64 encoding
261 @param modulename: The module name
262 @param b64enc: if True the function will encode the zip file with base64
263 @param src: Integrate the source files
265 @return: a stream to store in a file-like object
268 ap = get_module_path(str(modulename))
270 raise Exception('Unable to find path for module %s' % modulename)
272 ap = ap.encode('utf8')
273 if os.path.isfile(ap + '.zip'):
274 val = file(ap + '.zip', 'rb').read()
276 val = base64.encodestring(val)
278 val = zip_directory(ap, b64enc, src)
283 def get_module_resource(module, *args):
284 """Return the full path of a resource of the given module.
286 @param module: the module
287 @param args: the resource path components
289 @return: absolute path to the resource
291 TODO name it get_resource_path
292 TODO make it available inside on osv object (self.get_resource_path)
294 a = get_module_path(module)
295 if not a: return False
296 resource_path = opj(a, *args)
297 if zipfile.is_zipfile( a +'.zip') :
298 zip = zipfile.ZipFile( a + ".zip")
299 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
300 resource_path = '/'.join(args)
301 if resource_path in files:
302 return opj(a, resource_path)
303 elif os.path.exists(resource_path):
307 def get_module_icon(module):
308 iconpath = ['static', 'src', 'img', 'icon.png']
309 if get_module_resource(module, *iconpath):
310 return ('/' + module + '/') + '/'.join(iconpath)
311 return '/base/' + '/'.join(iconpath)
313 def load_information_from_description_file(module):
315 :param module: The name of the module (sale, purchase, ...)
318 terp_file = get_module_resource(module, '__openerp__.py')
320 terp_file = get_module_resource(module, '__terp__.py')
321 mod_path = get_module_path(module)
324 if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
325 # default values for descriptor
327 'application': False,
329 'auto_install': False,
330 'category': 'Uncategorized',
334 'icon': get_module_icon(module),
336 'auto_install': False,
345 info.update(itertools.izip(
346 'depends data demo test init_xml update_xml demo_xml'.split(),
349 f = tools.file_open(terp_file)
351 info.update(eval(f.read()))
356 # 'active' has been renamed 'auto_install'
357 info['auto_install'] = info['active']
361 #TODO: refactor the logger in this file to follow the logging guidelines
363 _logger.debug('module %s: no descriptor file'
364 ' found: __openerp__.py or __terp__.py (deprecated)', module)
368 def init_module_models(cr, module_name, obj_list):
369 """ Initialize a list of models.
371 Call _auto_init and init on each model to create or update the
372 database tables supporting the models.
374 TODO better explanation of _auto_init and init.
377 _logger.info('module %s: creating or updating database tables', module_name)
380 result = obj._auto_init(cr, {'module': module_name})
383 if hasattr(obj, 'init'):
387 obj._auto_end(cr, {'module': module_name})
394 def load_openerp_module(module_name):
395 """ Load an OpenERP module, if not already loaded.
397 This loads the module and register all of its models, thanks to either
398 the MetaModel metaclass, or the explicit instantiation of the model.
399 This is also used to load server-wide module (i.e. it is also used
400 when there is no model to register).
403 if module_name in loaded:
406 initialize_sys_path()
408 mod_path = get_module_path(module_name)
409 zip_mod_path = mod_path + '.zip'
410 if not os.path.isfile(zip_mod_path):
411 __import__('openerp.addons.' + module_name)
413 zimp = zipimport.zipimporter(zip_mod_path)
414 zimp.load_module(module_name)
416 # Call the module's post-load hook. This can done before any model or
417 # data has been initialized. This is ok as the post-load hook is for
418 # server-wide (instead of registry-specific) functionalities.
419 info = load_information_from_description_file(module_name)
420 if info['post_load']:
421 getattr(sys.modules['openerp.addons.' + module_name], info['post_load'])()
424 mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
425 msg = "Couldn't load %smodule %s" % (mt, module_name)
426 _logger.critical(msg)
430 loaded.append(module_name)
433 """Returns the list of module names
437 name = os.path.basename(name)
438 if name[-4:] == '.zip':
442 def is_really_module(name):
443 name = opj(dir, name)
444 return os.path.isdir(name) or zipfile.is_zipfile(name)
445 return map(clean, filter(is_really_module, os.listdir(dir)))
448 initialize_sys_path()
450 plist.extend(listdir(ad))
451 return list(set(plist))
454 def get_modules_with_version():
455 modules = get_modules()
457 for module in modules:
459 info = load_information_from_description_file(module)
460 res[module] = "%s.%s" % (release.major_version, info['version'])
465 def get_test_modules(module, submodule, explode):
467 Return a list of submodules containing tests.
470 - the name of a submodule
472 - '__sanity_checks__'
474 # Turn command-line module, submodule into importable names.
477 elif module == 'openerp':
478 module = 'openerp.tests'
480 module = 'openerp.addons.' + module + '.tests'
482 # Try to import the module
487 print 'Can not `import %s`.' % module
489 logging.exception('')
492 if str(e) == 'No module named tests':
493 # It seems the module has no `tests` sub-module, no problem.
496 _logger.exception('Can not `import %s`.', module)
499 # Discover available test sub-modules.
500 m = sys.modules[module]
501 submodule_names = sorted([x for x in dir(m) \
502 if x.startswith('test_') and \
503 isinstance(getattr(m, x), types.ModuleType)])
504 submodules = [getattr(m, x) for x in submodule_names]
506 def show_submodules_and_exit():
508 print 'Available submodules are:'
509 for x in submodule_names:
513 if submodule is None:
514 # Use auto-discovered sub-modules.
516 elif submodule == '__fast_suite__':
517 # Obtain the explicit test sub-modules list.
518 ms = getattr(sys.modules[module], 'fast_suite', None)
519 # `suite` was used before the 6.1 release instead of `fast_suite`.
520 ms = ms if ms else getattr(sys.modules[module], 'suite', None)
523 print 'The module `%s` has no defined test suite.' % (module,)
524 show_submodules_and_exit()
527 elif submodule == '__sanity_checks__':
528 ms = getattr(sys.modules[module], 'checks', None)
531 print 'The module `%s` has no defined sanity checks.' % (module,)
532 show_submodules_and_exit()
536 # Pick the command-line-specified test sub-module.
537 m = getattr(sys.modules[module], submodule, None)
542 print 'The module `%s` has no submodule named `%s`.' % \
544 show_submodules_and_exit()
550 def run_unit_tests(module_name):
552 Return True or False if some tests were found and succeeded or failed.
553 Return None if no test was found.
556 ms = get_test_modules(module_name, '__fast_suite__', explode=False)
557 # TODO: No need to try again if the above call failed because of e.g. a syntax error.
558 ms.extend(get_test_modules(module_name, '__sanity_checks__', explode=False))
559 suite = unittest2.TestSuite()
561 suite.addTests(unittest2.TestLoader().loadTestsFromModule(m))
563 _logger.info('module %s: executing %s `fast_suite` and/or `checks` sub-modules', module_name, len(ms))
564 # Use a custom stream object to log the test executions.
565 class MyStream(object):
567 self.r = re.compile(r'^-*$|^ *... *$|^ok$')
574 for c in s.split('\n'):
578 _logger.log(logging.TEST, c)
579 result = unittest2.TextTestRunner(verbosity=2, stream=MyStream()).run(suite)
580 if result.wasSuccessful():
583 _logger.error('module %s: at least one error occurred in a test', module_name)
586 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: