[FIX] correct uses of addons_path
[odoo/odoo.git] / openerp / modules / module.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
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>).
7 #
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.
12 #
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.
17 #
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/>.
20 #
21 ##############################################################################
22
23 import imp
24 import itertools
25 import os
26 from os.path import join as opj
27 import sys
28 import types
29 import zipimport
30
31 import openerp.tools as tools
32 import openerp.tools.osutil as osutil
33 from openerp.tools.safe_eval import safe_eval as eval
34
35 import zipfile
36 import openerp.release as release
37
38 import re
39 import base64
40 from zipfile import PyZipFile, ZIP_DEFLATED
41 from cStringIO import StringIO
42
43 import logging
44
45 _logger = logging.getLogger(__name__)
46 _test_logger = logging.getLogger('openerp.tests')
47
48 ad_paths = []
49
50 # Modules already loaded
51 loaded = []
52
53 _logger = logging.getLogger(__name__)
54
55 class AddonsImportHook(object):
56     """
57     Import hook to load OpenERP addons from multiple paths.
58
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`.
65     """
66
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.
71
72     def load_module(self, module_name):
73
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]
79
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
84         return mod
85
86 def initialize_sys_path():
87     """
88     Setup an import-hook to be able to import OpenERP addons from the different
89     addons paths.
90
91     This ensures something like ``import crm`` (or even
92     ``import openerp.addons.crm``) works even if the addons are not in the
93     PYTHONPATH.
94     """
95     global ad_paths
96     if ad_paths:
97         return
98
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(','))
101
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]
105
106     sys.meta_path.append(AddonsImportHook())
107
108 def get_module_path(module, downloaded=False, display_warning=True):
109     """Return the path of the given module.
110
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.
114
115     """
116     initialize_sys_path()
117     for adp in ad_paths:
118         if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
119             return opj(adp, module)
120
121     if downloaded:
122         return opj(tools.config.addons_data_dir, module)
123     if display_warning:
124         _logger.warning('module %s: module not found', module)
125     return False
126
127
128 def get_module_filetree(module, dir='.'):
129     path = get_module_path(module)
130     if not path:
131         return False
132
133     dir = os.path.normpath(dir)
134     if dir == '.':
135         dir = ''
136     if dir.startswith('..') or (dir and dir[0] == '/'):
137         raise Exception('Cannot access file outside the module')
138
139     if not os.path.isdir(path):
140         # zipmodule
141         zip = zipfile.ZipFile(path + ".zip")
142         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
143     else:
144         files = osutil.listdir(path, True)
145
146     tree = {}
147     for f in files:
148         if not f.startswith(dir):
149             continue
150
151         if dir:
152             f = f[len(dir)+int(not dir.endswith('/')):]
153         lst = f.split(os.sep)
154         current = tree
155         while len(lst) != 1:
156             current = current.setdefault(lst.pop(0), {})
157         current[lst.pop(0)] = None
158
159     return tree
160
161 def zip_directory(directory, b64enc=True, src=True):
162     """Compress a directory
163
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
167
168     @return: a string containing the zip file
169     """
170
171     RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
172
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))
180
181     archname = StringIO()
182     archive = PyZipFile(archname, "w", ZIP_DEFLATED)
183
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')
186
187     archive.writepy(directory)
188     _zippy(archive, directory, src=src)
189     archive.close()
190     archive_data = archname.getvalue()
191     archname.close()
192
193     if b64enc:
194         return base64.encodestring(archive_data)
195
196     return archive_data
197
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
200
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
204
205     @return: a stream to store in a file-like object
206     """
207
208     ap = get_module_path(str(modulename))
209     if not ap:
210         raise Exception('Unable to find path for module %s' % modulename)
211
212     ap = ap.encode('utf8')
213     if os.path.isfile(ap + '.zip'):
214         val = file(ap + '.zip', 'rb').read()
215         if b64enc:
216             val = base64.encodestring(val)
217     else:
218         val = zip_directory(ap, b64enc, src)
219
220     return val
221
222
223 def get_module_resource(module, *args):
224     """Return the full path of a resource of the given module.
225
226     :param module: module name
227     :param list(str) args: resource path components within module
228
229     :rtype: str
230     :return: absolute path to the resource
231
232     TODO name it get_resource_path
233     TODO make it available inside on osv object (self.get_resource_path)
234     """
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):
241             return 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)
248     return False
249
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)
255
256 def load_information_from_description_file(module):
257     """
258     :param module: The name of the module (sale, purchase, ...)
259     """
260
261     terp_file = get_module_resource(module, '__openerp__.py')
262     mod_path = get_module_path(module)
263     if terp_file:
264         info = {}
265         if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
266             # default values for descriptor
267             info = {
268                 'application': False,
269                 'author': '',
270                 'auto_install': False,
271                 'category': 'Uncategorized',
272                 'depends': [],
273                 'description': '',
274                 'icon': get_module_icon(module),
275                 'installable': True,
276                 'license': 'AGPL-3',
277                 'name': False,
278                 'post_load': None,
279                 'version': '1.0',
280                 'web': False,
281                 'website': '',
282                 'sequence': 100,
283                 'summary': '',
284             }
285             info.update(itertools.izip(
286                 'depends data demo test init_xml update_xml demo_xml'.split(),
287                 iter(list, None)))
288
289             f = tools.file_open(terp_file)
290             try:
291                 info.update(eval(f.read()))
292             finally:
293                 f.close()
294
295             if 'active' in info:
296                 # 'active' has been renamed 'auto_install'
297                 info['auto_install'] = info['active']
298
299             info['version'] = adapt_version(info['version'])
300             return info
301
302     #TODO: refactor the logger in this file to follow the logging guidelines
303     #      for 6.0
304     _logger.debug('module %s: no __openerp__.py file found.', module)
305     return {}
306
307
308 def init_module_models(cr, module_name, obj_list):
309     """ Initialize a list of models.
310
311     Call _auto_init and init on each model to create or update the
312     database tables supporting the models.
313
314     TODO better explanation of _auto_init and init.
315
316     """
317     _logger.info('module %s: creating or updating database tables', module_name)
318     todo = []
319     for obj in obj_list:
320         result = obj._auto_init(cr, {'module': module_name})
321         if result:
322             todo += result
323         if hasattr(obj, 'init'):
324             obj.init(cr)
325         cr.commit()
326     for obj in obj_list:
327         obj._auto_end(cr, {'module': module_name})
328         cr.commit()
329     todo.sort()
330     for t in todo:
331         t[1](cr, *t[2])
332     cr.commit()
333
334 def load_openerp_module(module_name):
335     """ Load an OpenERP module, if not already loaded.
336
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).
341     """
342     global loaded
343     if module_name in loaded:
344         return
345
346     initialize_sys_path()
347     try:
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)
352         else:
353             zimp = zipimport.zipimporter(zip_mod_path)
354             zimp.load_module(module_name)
355
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'])()
362
363     except Exception, e:
364         mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
365         msg = "Couldn't load %smodule %s" % (mt, module_name)
366         _logger.critical(msg)
367         _logger.critical(e)
368         raise
369     else:
370         loaded.append(module_name)
371
372 def get_modules():
373     """Returns the list of module names
374     """
375     def listdir(dir):
376         def clean(name):
377             name = os.path.basename(name)
378             if name[-4:] == '.zip':
379                 name = name[:-4]
380             return name
381
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)))
387
388     plist = []
389     initialize_sys_path()
390     for ad in ad_paths:
391         plist.extend(listdir(ad))
392     return list(set(plist))
393
394
395 def get_modules_with_version():
396     modules = get_modules()
397     res = dict.fromkeys(modules, adapt_version('1.0'))
398     for module in modules:
399         try:
400             info = load_information_from_description_file(module)
401             res[module] = info['version']
402         except Exception:
403             continue
404     return res
405
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)
410     return version
411
412
413 def get_test_modules(module, submodule, explode):
414     """
415     Return a list of submodules containing tests.
416     `submodule` can be:
417       - None
418       - the name of a submodule
419       - '__fast_suite__'
420       - '__sanity_checks__'
421     """
422     # Turn command-line module, submodule into importable names.
423     if module is None:
424         pass
425     elif module == 'openerp':
426         module = 'openerp.tests'
427     else:
428         module = 'openerp.addons.' + module + '.tests'
429
430     # Try to import the module
431     try:
432         __import__(module)
433     except Exception, e:
434         if explode:
435             print 'Can not `import %s`.' % module
436             import logging
437             logging.exception('')
438             sys.exit(1)
439         else:
440             if str(e) == 'No module named tests':
441                 # It seems the module has no `tests` sub-module, no problem.
442                 pass
443             else:
444                 _logger.exception('Can not `import %s`.', module)
445             return []
446
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]
453
454     def show_submodules_and_exit():
455         if submodule_names:
456             print 'Available submodules are:'
457             for x in submodule_names:
458                 print ' ', x
459         sys.exit(1)
460
461     if submodule is None:
462         # Use auto-discovered sub-modules.
463         ms = submodules
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)
469         if ms is None:
470             if explode:
471                 print 'The module `%s` has no defined test suite.' % (module,)
472                 show_submodules_and_exit()
473             else:
474                 ms = []
475     elif submodule == '__sanity_checks__':
476         ms = getattr(sys.modules[module], 'checks', None)
477         if ms is None:
478             if explode:
479                 print 'The module `%s` has no defined sanity checks.' % (module,)
480                 show_submodules_and_exit()
481             else:
482                 ms = []
483     else:
484         # Pick the command-line-specified test sub-module.
485         m = getattr(sys.modules[module], submodule, None)
486         ms = [m]
487
488         if m is None:
489             if explode:
490                 print 'The module `%s` has no submodule named `%s`.' % \
491                     (module, submodule)
492                 show_submodules_and_exit()
493             else:
494                 ms = []
495
496     return ms
497
498 def run_unit_tests(module_name):
499     """
500     Return True or False if some tests were found and succeeded or failed.
501     Return None if no test was found.
502     """
503     import unittest2
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()
508     for m in ms:
509         suite.addTests(unittest2.TestLoader().loadTestsFromModule(m))
510     if ms:
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):
514             def __init__(self):
515                 self.r = re.compile(r'^-*$|^ *... *$|^ok$')
516             def flush(self):
517                 pass
518             def write(self, s):
519                 if self.r.match(s):
520                     return
521                 first = True
522                 for c in s.split('\n'):
523                     if not first:
524                         c = '` ' + c
525                     first = False
526                     _test_logger.info(c)
527         result = unittest2.TextTestRunner(verbosity=2, stream=MyStream()).run(suite)
528         if result.wasSuccessful():
529             return True
530         else:
531             _logger.error('module %s: at least one error occurred in a test', module_name)
532             return False
533
534 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: