[IMP] cli first command testjs
[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-2011 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': '0.0.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             return info
353
354     #TODO: refactor the logger in this file to follow the logging guidelines
355     #      for 6.0
356     _logger.debug('module %s: no descriptor file'
357         ' found: __openerp__.py or __terp__.py (deprecated)', module)
358     return {}
359
360
361 def init_module_models(cr, module_name, obj_list):
362     """ Initialize a list of models.
363
364     Call _auto_init and init on each model to create or update the
365     database tables supporting the models.
366
367     TODO better explanation of _auto_init and init.
368
369     """
370     _logger.info('module %s: creating or updating database tables', module_name)
371     todo = []
372     for obj in obj_list:
373         result = obj._auto_init(cr, {'module': module_name})
374         if result:
375             todo += result
376         if hasattr(obj, 'init'):
377             obj.init(cr)
378         cr.commit()
379     for obj in obj_list:
380         obj._auto_end(cr, {'module': module_name})
381         cr.commit()
382     todo.sort()
383     for t in todo:
384         t[1](cr, *t[2])
385     cr.commit()
386
387 def load_openerp_module(module_name):
388     """ Load an OpenERP module, if not already loaded.
389
390     This loads the module and register all of its models, thanks to either
391     the MetaModel metaclass, or the explicit instantiation of the model.
392     This is also used to load server-wide module (i.e. it is also used
393     when there is no model to register).
394     """
395     global loaded
396     if module_name in loaded:
397         return
398
399     initialize_sys_path()
400     try:
401         mod_path = get_module_path(module_name)
402         zip_mod_path = mod_path + '.zip'
403         if not os.path.isfile(zip_mod_path):
404             __import__('openerp.addons.' + module_name)
405         else:
406             zimp = zipimport.zipimporter(zip_mod_path)
407             zimp.load_module(module_name)
408
409         # Call the module's post-load hook. This can done before any model or
410         # data has been initialized. This is ok as the post-load hook is for
411         # server-wide (instead of registry-specific) functionalities.
412         info = load_information_from_description_file(module_name)
413         if info['post_load']:
414             getattr(sys.modules['openerp.addons.' + module_name], info['post_load'])()
415
416     except Exception, e:
417         mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
418         msg = "Couldn't load %smodule %s" % (mt, module_name)
419         _logger.critical(msg)
420         _logger.critical(e)
421         raise
422     else:
423         loaded.append(module_name)
424
425 def get_modules():
426     """Returns the list of module names
427     """
428     def listdir(dir):
429         def clean(name):
430             name = os.path.basename(name)
431             if name[-4:] == '.zip':
432                 name = name[:-4]
433             return name
434
435         def is_really_module(name):
436             manifest_name = opj(dir, name, '__openerp__.py')
437             zipfile_name = opj(dir, name)
438             return os.path.isfile(manifest_name) or zipfile.is_zipfile(zipfile_name)
439         return map(clean, filter(is_really_module, os.listdir(dir)))
440
441     plist = []
442     initialize_sys_path()
443     for ad in ad_paths:
444         plist.extend(listdir(ad))
445     return list(set(plist))
446
447
448 def get_modules_with_version():
449     modules = get_modules()
450     res = {}
451     for module in modules:
452         try:
453             info = load_information_from_description_file(module)
454             res[module] = "%s.%s" % (release.major_version, info['version'])
455         except Exception, e:
456             continue
457     return res
458
459 def get_test_modules(module, submodule, explode):
460     """
461     Return a list of submodules containing tests.
462     `submodule` can be:
463       - None
464       - the name of a submodule
465       - '__fast_suite__'
466       - '__sanity_checks__'
467     """
468     # Turn command-line module, submodule into importable names.
469     if module is None:
470         pass
471     elif module == 'openerp':
472         module = 'openerp.tests'
473     else:
474         module = 'openerp.addons.' + module + '.tests'
475
476     # Try to import the module
477     try:
478         __import__(module)
479     except Exception, e:
480         if explode:
481             print 'Can not `import %s`.' % module
482             import logging
483             logging.exception('')
484             sys.exit(1)
485         else:
486             if str(e) == 'No module named tests':
487                 # It seems the module has no `tests` sub-module, no problem.
488                 pass
489             else:
490                 _logger.exception('Can not `import %s`.', module)
491             return []
492
493     # Discover available test sub-modules.
494     m = sys.modules[module]
495     submodule_names =  sorted([x for x in dir(m) \
496         if x.startswith('test_') and \
497         isinstance(getattr(m, x), types.ModuleType)])
498     submodules = [getattr(m, x) for x in submodule_names]
499
500     def show_submodules_and_exit():
501         if submodule_names:
502             print 'Available submodules are:'
503             for x in submodule_names:
504                 print ' ', x
505         sys.exit(1)
506
507     if submodule is None:
508         # Use auto-discovered sub-modules.
509         ms = submodules
510     elif submodule == '__fast_suite__':
511         # Obtain the explicit test sub-modules list.
512         ms = getattr(sys.modules[module], 'fast_suite', None)
513         # `suite` was used before the 6.1 release instead of `fast_suite`.
514         ms = ms if ms else getattr(sys.modules[module], 'suite', None)
515         if ms is None:
516             if explode:
517                 print 'The module `%s` has no defined test suite.' % (module,)
518                 show_submodules_and_exit()
519             else:
520                 ms = []
521     elif submodule == '__sanity_checks__':
522         ms = getattr(sys.modules[module], 'checks', None)
523         if ms is None:
524             if explode:
525                 print 'The module `%s` has no defined sanity checks.' % (module,)
526                 show_submodules_and_exit()
527             else:
528                 ms = []
529     else:
530         # Pick the command-line-specified test sub-module.
531         m = getattr(sys.modules[module], submodule, None)
532         ms = [m]
533
534         if m is None:
535             if explode:
536                 print 'The module `%s` has no submodule named `%s`.' % \
537                     (module, submodule)
538                 show_submodules_and_exit()
539             else:
540                 ms = []
541
542     return ms
543
544 def run_unit_tests(module_name):
545     """
546     Return True or False if some tests were found and succeeded or failed.
547     Return None if no test was found.
548     """
549     import unittest2
550     ms = get_test_modules(module_name, '__fast_suite__', explode=False)
551     # TODO: No need to try again if the above call failed because of e.g. a syntax error.
552     ms.extend(get_test_modules(module_name, '__sanity_checks__', explode=False))
553     suite = unittest2.TestSuite()
554     for m in ms:
555         suite.addTests(unittest2.TestLoader().loadTestsFromModule(m))
556     if ms:
557         _logger.log(logging.TEST, 'module %s: executing %s `fast_suite` and/or `checks` sub-modules', module_name, len(ms))
558         # Use a custom stream object to log the test executions.
559         class MyStream(object):
560             def __init__(self):
561                 self.r = re.compile(r'^-*$|^ *... *$|^ok$')
562             def flush(self):
563                 pass
564             def write(self, s):
565                 if self.r.match(s):
566                     return
567                 first = True
568                 for c in s.split('\n'):
569                     if not first:
570                         c = '` ' + c
571                     first = False
572                     _logger.log(logging.TEST, c)
573         result = unittest2.TextTestRunner(verbosity=2, stream=MyStream()).run(suite)
574         if result.wasSuccessful():
575             return True
576         else:
577             _logger.error('module %s: at least one error occurred in a test', module_name)
578             return False
579
580 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: