Merge pull request #648 from odoo-dev/7.0-fix-searchbar-navigation-ged
[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-2012 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
47 _ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base)
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     For backward compatibility, loading an addons puts it in `sys.modules`
67     under both the legacy (short) name, and the new (longer) name. This
68     ensures that
69         import hr
70         import openerp.addons.hr
71     loads the hr addons only once.
72
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.
77
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.
82     """
83
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.
88
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):
94             try:
95                 # Check if the bare module name clashes with another module.
96                 f, path, descr = imp.find_module(module_parts[0])
97                 _logger.warning("""
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))
101                 return
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.
107
108     def load_module(self, module_name):
109
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]
115
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]
120
121         try:
122             # Check if the bare module name shadows another module.
123             f, path, descr = imp.find_module(module_part)
124             is_shadowing = True
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.
129             is_shadowing = False
130
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)
134         if not is_shadowing:
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
140         return mod
141
142 def initialize_sys_path():
143     """
144     Setup an import-hook to be able to import OpenERP addons from the different
145     addons paths.
146
147     This ensures something like ``import crm`` (or even
148     ``import openerp.addons.crm``) works even if the addons are not in the
149     PYTHONPATH.
150     """
151     global ad_paths
152     if ad_paths:
153         return
154
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())
158
159 def get_module_path(module, downloaded=False, display_warning=True):
160     """Return the path of the given module.
161
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.
165
166     """
167     initialize_sys_path()
168     for adp in ad_paths:
169         if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
170             return opj(adp, module)
171
172     if downloaded:
173         return opj(_ad, module)
174     if display_warning:
175         _logger.warning('module %s: module not found', module)
176     return False
177
178
179 def get_module_filetree(module, dir='.'):
180     path = get_module_path(module)
181     if not path:
182         return False
183
184     dir = os.path.normpath(dir)
185     if dir == '.':
186         dir = ''
187     if dir.startswith('..') or (dir and dir[0] == '/'):
188         raise Exception('Cannot access file outside the module')
189
190     if not os.path.isdir(path):
191         # zipmodule
192         zip = zipfile.ZipFile(path + ".zip")
193         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
194     else:
195         files = osutil.listdir(path, True)
196
197     tree = {}
198     for f in files:
199         if not f.startswith(dir):
200             continue
201
202         if dir:
203             f = f[len(dir)+int(not dir.endswith('/')):]
204         lst = f.split(os.sep)
205         current = tree
206         while len(lst) != 1:
207             current = current.setdefault(lst.pop(0), {})
208         current[lst.pop(0)] = None
209
210     return tree
211
212 def zip_directory(directory, b64enc=True, src=True):
213     """Compress a directory
214
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
218
219     @return: a string containing the zip file
220     """
221
222     RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
223
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))
231
232     archname = StringIO()
233     archive = PyZipFile(archname, "w", ZIP_DEFLATED)
234
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')
237
238     archive.writepy(directory)
239     _zippy(archive, directory, src=src)
240     archive.close()
241     archive_data = archname.getvalue()
242     archname.close()
243
244     if b64enc:
245         return base64.encodestring(archive_data)
246
247     return archive_data
248
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
251
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
255
256     @return: a stream to store in a file-like object
257     """
258
259     ap = get_module_path(str(modulename))
260     if not ap:
261         raise Exception('Unable to find path for module %s' % modulename)
262
263     ap = ap.encode('utf8')
264     if os.path.isfile(ap + '.zip'):
265         val = file(ap + '.zip', 'rb').read()
266         if b64enc:
267             val = base64.encodestring(val)
268     else:
269         val = zip_directory(ap, b64enc, src)
270
271     return val
272
273
274 def get_module_resource(module, *args):
275     """Return the full path of a resource of the given module.
276
277     :param module: module name
278     :param list(str) args: resource path components within module
279
280     :rtype: str
281     :return: absolute path to the resource
282
283     TODO name it get_resource_path
284     TODO make it available inside on osv object (self.get_resource_path)
285     """
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):
292             return 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)
299     return False
300
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)
306
307 def load_information_from_description_file(module):
308     """
309     :param module: The name of the module (sale, purchase, ...)
310     """
311
312     terp_file = get_module_resource(module, '__openerp__.py')
313     if not terp_file:
314         terp_file = get_module_resource(module, '__terp__.py')
315     mod_path = get_module_path(module)
316     if terp_file:
317         info = {}
318         if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
319             # default values for descriptor
320             info = {
321                 'application': False,
322                 'author': '',
323                 'auto_install': False,
324                 'category': 'Uncategorized',
325                 'depends': [],
326                 'description': '',
327                 'icon': get_module_icon(module),
328                 'installable': True,
329                 'license': 'AGPL-3',
330                 'name': False,
331                 'post_load': None,
332                 'version': '1.0',
333                 'web': False,
334                 'website': '',
335                 'sequence': 100,
336                 'summary': '',
337             }
338             info.update(itertools.izip(
339                 'depends data demo test init_xml update_xml demo_xml'.split(),
340                 iter(list, None)))
341
342             f = tools.file_open(terp_file)
343             try:
344                 info.update(eval(f.read()))
345             finally:
346                 f.close()
347
348             if 'active' in info:
349                 # 'active' has been renamed 'auto_install'
350                 info['auto_install'] = info['active']
351
352             info['version'] = adapt_version(info['version'])
353             return info
354
355     #TODO: refactor the logger in this file to follow the logging guidelines
356     #      for 6.0
357     _logger.debug('module %s: no descriptor file'
358         ' found: __openerp__.py or __terp__.py (deprecated)', module)
359     return {}
360
361
362 def init_module_models(cr, module_name, obj_list):
363     """ Initialize a list of models.
364
365     Call _auto_init and init on each model to create or update the
366     database tables supporting the models.
367
368     TODO better explanation of _auto_init and init.
369
370     """
371     _logger.info('module %s: creating or updating database tables', module_name)
372     todo = []
373     for obj in obj_list:
374         result = obj._auto_init(cr, {'module': module_name})
375         if result:
376             todo += result
377         if hasattr(obj, 'init'):
378             obj.init(cr)
379         cr.commit()
380     for obj in obj_list:
381         obj._auto_end(cr, {'module': module_name})
382         cr.commit()
383     todo.sort()
384     for t in todo:
385         t[1](cr, *t[2])
386     cr.commit()
387
388 def load_openerp_module(module_name):
389     """ Load an OpenERP module, if not already loaded.
390
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).
395     """
396     global loaded
397     if module_name in loaded:
398         return
399
400     initialize_sys_path()
401     try:
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)
406         else:
407             zimp = zipimport.zipimporter(zip_mod_path)
408             zimp.load_module(module_name)
409
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'])()
416
417     except Exception, e:
418         mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
419         msg = "Couldn't load %smodule %s" % (mt, module_name)
420         _logger.critical(msg)
421         _logger.critical(e)
422         raise
423     else:
424         loaded.append(module_name)
425
426 def get_modules():
427     """Returns the list of module names
428     """
429     def listdir(dir):
430         def clean(name):
431             name = os.path.basename(name)
432             if name[-4:] == '.zip':
433                 name = name[:-4]
434             return name
435
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)))
441
442     plist = []
443     initialize_sys_path()
444     for ad in ad_paths:
445         plist.extend(listdir(ad))
446     return list(set(plist))
447
448
449 def get_modules_with_version():
450     modules = get_modules()
451     res = dict.fromkeys(modules, adapt_version('1.0'))
452     for module in modules:
453         try:
454             info = load_information_from_description_file(module)
455             res[module] = info['version']
456         except Exception:
457             continue
458     return res
459
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)
464     return version
465
466
467 def get_test_modules(module, submodule, explode):
468     """
469     Return a list of submodules containing tests.
470     `submodule` can be:
471       - None
472       - the name of a submodule
473       - '__fast_suite__'
474       - '__sanity_checks__'
475     """
476     # Turn command-line module, submodule into importable names.
477     if module is None:
478         pass
479     elif module == 'openerp':
480         module = 'openerp.tests'
481     else:
482         module = 'openerp.addons.' + module + '.tests'
483
484     # Try to import the module
485     try:
486         __import__(module)
487     except Exception, e:
488         if explode:
489             print 'Can not `import %s`.' % module
490             import logging
491             logging.exception('')
492             sys.exit(1)
493         else:
494             if str(e) == 'No module named tests':
495                 # It seems the module has no `tests` sub-module, no problem.
496                 pass
497             else:
498                 _logger.exception('Can not `import %s`.', module)
499             return []
500
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]
507
508     def show_submodules_and_exit():
509         if submodule_names:
510             print 'Available submodules are:'
511             for x in submodule_names:
512                 print ' ', x
513         sys.exit(1)
514
515     if submodule is None:
516         # Use auto-discovered sub-modules.
517         ms = submodules
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)
523         if ms is None:
524             if explode:
525                 print 'The module `%s` has no defined test suite.' % (module,)
526                 show_submodules_and_exit()
527             else:
528                 ms = []
529     elif submodule == '__sanity_checks__':
530         ms = getattr(sys.modules[module], 'checks', None)
531         if ms is None:
532             if explode:
533                 print 'The module `%s` has no defined sanity checks.' % (module,)
534                 show_submodules_and_exit()
535             else:
536                 ms = []
537     else:
538         # Pick the command-line-specified test sub-module.
539         m = getattr(sys.modules[module], submodule, None)
540         ms = [m]
541
542         if m is None:
543             if explode:
544                 print 'The module `%s` has no submodule named `%s`.' % \
545                     (module, submodule)
546                 show_submodules_and_exit()
547             else:
548                 ms = []
549
550     return ms
551
552 def run_unit_tests(module_name):
553     """
554     Return True or False if some tests were found and succeeded or failed.
555     Return None if no test was found.
556     """
557     import unittest2
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()
562     for m in ms:
563         suite.addTests(unittest2.TestLoader().loadTestsFromModule(m))
564     if ms:
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):
568             def __init__(self):
569                 self.r = re.compile(r'^-*$|^ *... *$|^ok$')
570             def flush(self):
571                 pass
572             def write(self, s):
573                 if self.r.match(s):
574                     return
575                 first = True
576                 for c in s.split('\n'):
577                     if not first:
578                         c = '` ' + c
579                     first = False
580                     _logger.log(logging.TEST, c)
581         result = unittest2.TextTestRunner(verbosity=2, stream=MyStream()).run(suite)
582         if result.wasSuccessful():
583             return True
584         else:
585             _logger.error('module %s: at least one error occurred in a test', module_name)
586             return False
587
588 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: