Merge branch 'master' of https://github.com/odoo/odoo
[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 functools
24 import imp
25 import itertools
26 import logging
27 import os
28 import re
29 import sys
30 import time
31 import unittest
32 from os.path import join as opj
33
34 import unittest2
35
36 import openerp
37 import openerp.tools as tools
38 import openerp.release as release
39 from openerp.tools.safe_eval import safe_eval as eval
40
41 MANIFEST = '__openerp__.py'
42 README = ['README.rst', 'README.md', 'README.txt']
43
44 _logger = logging.getLogger(__name__)
45
46 # addons path as a list
47 ad_paths = []
48 hooked = False
49
50 # Modules already loaded
51 loaded = []
52
53 class AddonsImportHook(object):
54     """
55     Import hook to load OpenERP addons from multiple paths.
56
57     OpenERP implements its own import-hook to load its addons. OpenERP
58     addons are Python modules. Originally, they were each living in their
59     own top-level namespace, e.g. the sale module, or the hr module. For
60     backward compatibility, `import <module>` is still supported. Now they
61     are living in `openerp.addons`. The good way to import such modules is
62     thus `import openerp.addons.module`.
63     """
64
65     def find_module(self, module_name, package_path):
66         module_parts = module_name.split('.')
67         if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
68             return self # We act as a loader too.
69
70     def load_module(self, module_name):
71         if module_name in sys.modules:
72             return sys.modules[module_name]
73
74         _1, _2, module_part = module_name.split('.')
75         # Note: we don't support circular import.
76         f, path, descr = imp.find_module(module_part, ad_paths)
77         mod = imp.load_module('openerp.addons.' + module_part, f, path, descr)
78         sys.modules['openerp.addons.' + module_part] = mod
79         return mod
80
81 def initialize_sys_path():
82     """
83     Setup an import-hook to be able to import OpenERP addons from the different
84     addons paths.
85
86     This ensures something like ``import crm`` (or even
87     ``import openerp.addons.crm``) works even if the addons are not in the
88     PYTHONPATH.
89     """
90     global ad_paths
91     global hooked
92
93     dd = tools.config.addons_data_dir
94     if dd not in ad_paths:
95         ad_paths.append(dd)
96
97     for ad in tools.config['addons_path'].split(','):
98         ad = os.path.abspath(tools.ustr(ad.strip()))
99         if ad not in ad_paths:
100             ad_paths.append(ad)
101
102     # add base module path
103     base_path = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons'))
104     if base_path not in ad_paths:
105         ad_paths.append(base_path)
106
107     if not hooked:
108         sys.meta_path.append(AddonsImportHook())
109         hooked = True
110
111 def get_module_path(module, downloaded=False, display_warning=True):
112     """Return the path of the given module.
113
114     Search the addons paths and return the first path where the given
115     module is found. If downloaded is True, return the default addons
116     path if nothing else is found.
117
118     """
119     initialize_sys_path()
120     for adp in ad_paths:
121         if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
122             return opj(adp, module)
123
124     if downloaded:
125         return opj(tools.config.addons_data_dir, module)
126     if display_warning:
127         _logger.warning('module %s: module not found', module)
128     return False
129
130 def get_module_filetree(module, dir='.'):
131     path = get_module_path(module)
132     if not path:
133         return False
134
135     dir = os.path.normpath(dir)
136     if dir == '.':
137         dir = ''
138     if dir.startswith('..') or (dir and dir[0] == '/'):
139         raise Exception('Cannot access file outside the module')
140
141     files = openerp.tools.osutil.listdir(path, True)
142
143     tree = {}
144     for f in files:
145         if not f.startswith(dir):
146             continue
147
148         if dir:
149             f = f[len(dir)+int(not dir.endswith('/')):]
150         lst = f.split(os.sep)
151         current = tree
152         while len(lst) != 1:
153             current = current.setdefault(lst.pop(0), {})
154         current[lst.pop(0)] = None
155
156     return tree
157
158 def get_resource_path(module, *args):
159     """Return the full path of a resource of the given module.
160
161     :param module: module name
162     :param list(str) args: resource path components within module
163
164     :rtype: str
165     :return: absolute path to the resource
166
167     TODO make it available inside on osv object (self.get_resource_path)
168     """
169     mod_path = get_module_path(module)
170     if not mod_path: return False
171     resource_path = opj(mod_path, *args)
172     if os.path.isdir(mod_path):
173         # the module is a directory - ignore zip behavior
174         if os.path.exists(resource_path):
175             return resource_path
176     return False
177
178 # backwards compatibility
179 get_module_resource = get_resource_path
180
181 def get_resource_from_path(path):
182     """Tries to extract the module name and the resource's relative path
183     out of an absolute resource path.
184
185     If operation is successfull, returns a tuple containing the module name, the relative path
186     to the resource using '/' as filesystem seperator[1] and the same relative path using
187     os.path.sep seperators.
188
189     [1] same convention as the resource path declaration in manifests
190
191     :param path: absolute resource path
192
193     :rtype: tuple
194     :return: tuple(module_name, relative_path, os_relative_path) if possible, else None
195     """
196     resource = [path.replace(adpath, '') for adpath in ad_paths if path.startswith(adpath)]
197     if resource:
198         relative = resource[0].split(os.path.sep)
199         if not relative[0]:
200             relative.pop(0)
201         module = relative.pop(0)
202         return (module, '/'.join(relative), os.path.sep.join(relative))
203     return None
204
205 def get_module_icon(module):
206     iconpath = ['static', 'description', 'icon.png']
207     if get_module_resource(module, *iconpath):
208         return ('/' + module + '/') + '/'.join(iconpath)
209     return '/base/'  + '/'.join(iconpath)
210
211 def get_module_root(path):
212     """
213     Get closest module's root begining from path
214
215         # Given:
216         # /foo/bar/module_dir/static/src/...
217
218         get_module_root('/foo/bar/module_dir/static/')
219         # returns '/foo/bar/module_dir'
220
221         get_module_root('/foo/bar/module_dir/')
222         # returns '/foo/bar/module_dir'
223
224         get_module_root('/foo/bar')
225         # returns None
226
227     @param path: Path from which the lookup should start
228
229     @return:  Module root path or None if not found
230     """
231     while not os.path.exists(os.path.join(path, MANIFEST)):
232         new_path = os.path.abspath(os.path.join(path, os.pardir))
233         if path == new_path:
234             return None
235         path = new_path
236     return path
237
238 def load_information_from_description_file(module, mod_path=None):
239     """
240     :param module: The name of the module (sale, purchase, ...)
241     :param mod_path: Physical path of module, if not providedThe name of the module (sale, purchase, ...)
242     """
243
244     if not mod_path:
245         mod_path = get_module_path(module)
246     terp_file = mod_path and opj(mod_path, MANIFEST) or False
247     if terp_file:
248         info = {}
249         if os.path.isfile(terp_file):
250             # default values for descriptor
251             info = {
252                 'application': False,
253                 'author': '',
254                 'auto_install': False,
255                 'category': 'Uncategorized',
256                 'depends': [],
257                 'description': '',
258                 'icon': get_module_icon(module),
259                 'installable': True,
260                 'license': 'AGPL-3',
261                 'post_load': None,
262                 'version': '1.0',
263                 'web': False,
264                 'website': '',
265                 'sequence': 100,
266                 'summary': '',
267             }
268             info.update(itertools.izip(
269                 'depends data demo test init_xml update_xml demo_xml'.split(),
270                 iter(list, None)))
271
272             f = tools.file_open(terp_file)
273             try:
274                 info.update(eval(f.read()))
275             finally:
276                 f.close()
277
278             if not info.get('description'):
279                 readme_path = [opj(mod_path, x) for x in README
280                                if os.path.isfile(opj(mod_path, x))]
281                 if readme_path:
282                     readme_text = tools.file_open(readme_path[0]).read()
283                     info['description'] = readme_text
284
285             if 'active' in info:
286                 # 'active' has been renamed 'auto_install'
287                 info['auto_install'] = info['active']
288
289             info['version'] = adapt_version(info['version'])
290             return info
291
292     #TODO: refactor the logger in this file to follow the logging guidelines
293     #      for 6.0
294     _logger.debug('module %s: no %s file found.', module, MANIFEST)
295     return {}
296
297 def init_module_models(cr, module_name, obj_list):
298     """ Initialize a list of models.
299
300     Call _auto_init and init on each model to create or update the
301     database tables supporting the models.
302
303     TODO better explanation of _auto_init and init.
304
305     """
306     _logger.info('module %s: creating or updating database tables', module_name)
307     todo = []
308     for obj in obj_list:
309         result = obj._auto_init(cr, {'module': module_name})
310         if result:
311             todo += result
312         if hasattr(obj, 'init'):
313             obj.init(cr)
314         cr.commit()
315     for obj in obj_list:
316         obj._auto_end(cr, {'module': module_name})
317         cr.commit()
318     todo.sort(key=lambda x: x[0])
319     for t in todo:
320         t[1](cr, *t[2])
321     cr.commit()
322
323 def load_openerp_module(module_name):
324     """ Load an OpenERP module, if not already loaded.
325
326     This loads the module and register all of its models, thanks to either
327     the MetaModel metaclass, or the explicit instantiation of the model.
328     This is also used to load server-wide module (i.e. it is also used
329     when there is no model to register).
330     """
331     global loaded
332     if module_name in loaded:
333         return
334
335     initialize_sys_path()
336     try:
337         mod_path = get_module_path(module_name)
338         __import__('openerp.addons.' + module_name)
339
340         # Call the module's post-load hook. This can done before any model or
341         # data has been initialized. This is ok as the post-load hook is for
342         # server-wide (instead of registry-specific) functionalities.
343         info = load_information_from_description_file(module_name)
344         if info['post_load']:
345             getattr(sys.modules['openerp.addons.' + module_name], info['post_load'])()
346
347     except Exception, e:
348         msg = "Couldn't load module %s" % (module_name)
349         _logger.critical(msg)
350         _logger.critical(e)
351         raise
352     else:
353         loaded.append(module_name)
354
355 def get_modules():
356     """Returns the list of module names
357     """
358     def listdir(dir):
359         def clean(name):
360             name = os.path.basename(name)
361             if name[-4:] == '.zip':
362                 name = name[:-4]
363             return name
364
365         def is_really_module(name):
366             manifest_name = opj(dir, name, MANIFEST)
367             zipfile_name = opj(dir, name)
368             return os.path.isfile(manifest_name)
369         return map(clean, filter(is_really_module, os.listdir(dir)))
370
371     plist = []
372     initialize_sys_path()
373     for ad in ad_paths:
374         plist.extend(listdir(ad))
375     return list(set(plist))
376
377 def get_modules_with_version():
378     modules = get_modules()
379     res = dict.fromkeys(modules, adapt_version('1.0'))
380     for module in modules:
381         try:
382             info = load_information_from_description_file(module)
383             res[module] = info['version']
384         except Exception:
385             continue
386     return res
387
388 def adapt_version(version):
389     serie = release.major_version
390     if version == serie or not version.startswith(serie + '.'):
391         version = '%s.%s' % (serie, version)
392     return version
393
394 def get_test_modules(module):
395     """ Return a list of module for the addons potentialy containing tests to
396     feed unittest2.TestLoader.loadTestsFromModule() """
397     # Try to import the module
398     module = 'openerp.addons.' + module + '.tests'
399     try:
400         __import__(module)
401     except Exception, e:
402         # If module has no `tests` sub-module, no problem.
403         if str(e) != 'No module named tests':
404             _logger.exception('Can not `import %s`.', module)
405         return []
406
407     # include submodules too
408     result = [mod_obj for name, mod_obj in sys.modules.iteritems()
409               if mod_obj # mod_obj can be None
410               if name.startswith(module)
411               if re.search(r'test_\w+$', name)]
412     return result
413
414 # Use a custom stream object to log the test executions.
415 class TestStream(object):
416     def __init__(self, logger_name='openerp.tests'):
417         self.logger = logging.getLogger(logger_name)
418         self.r = re.compile(r'^-*$|^ *... *$|^ok$')
419     def flush(self):
420         pass
421     def write(self, s):
422         if self.r.match(s):
423             return
424         first = True
425         level = logging.ERROR if s.startswith(('ERROR', 'FAIL', 'Traceback')) else logging.INFO
426         for c in s.splitlines():
427             if not first:
428                 c = '` ' + c
429             first = False
430             self.logger.log(level, c)
431
432 current_test = None
433
434 def runs_at(test, hook, default):
435     # by default, tests do not run post install
436     test_runs = getattr(test, hook, default)
437
438     # for a test suite, we're done
439     if not isinstance(test, unittest.TestCase):
440         return test_runs
441
442     # otherwise check the current test method to see it's been set to a
443     # different state
444     method = getattr(test, test._testMethodName)
445     return getattr(method, hook, test_runs)
446
447 runs_at_install = functools.partial(runs_at, hook='at_install', default=True)
448 runs_post_install = functools.partial(runs_at, hook='post_install', default=False)
449
450 def run_unit_tests(module_name, dbname, position=runs_at_install):
451     """
452     :returns: ``True`` if all of ``module_name``'s tests succeeded, ``False``
453               if any of them failed.
454     :rtype: bool
455     """
456     global current_test
457     current_test = module_name
458     mods = get_test_modules(module_name)
459     r = True
460     for m in mods:
461         tests = unwrap_suite(unittest2.TestLoader().loadTestsFromModule(m))
462         suite = unittest2.TestSuite(itertools.ifilter(position, tests))
463
464         if suite.countTestCases():
465             t0 = time.time()
466             t0_sql = openerp.sql_db.sql_counter
467             _logger.info('%s running tests.', m.__name__)
468             result = unittest2.TextTestRunner(verbosity=2, stream=TestStream(m.__name__)).run(suite)
469             if time.time() - t0 > 5:
470                 _logger.log(25, "%s tested in %.2fs, %s queries", m.__name__, time.time() - t0, openerp.sql_db.sql_counter - t0_sql)
471             if not result.wasSuccessful():
472                 r = False
473                 _logger.error("Module %s: %d failures, %d errors", module_name, len(result.failures), len(result.errors))
474
475     current_test = None
476     return r
477
478 def unwrap_suite(test):
479     """
480     Attempts to unpack testsuites (holding suites or cases) in order to
481     generate a single stream of terminals (either test cases or customized
482     test suites). These can then be checked for run/skip attributes
483     individually.
484
485     An alternative would be to use a variant of @unittest2.skipIf with a state
486     flag of some sort e.g. @unittest2.skipIf(common.runstate != 'at_install'),
487     but then things become weird with post_install as tests should *not* run
488     by default there
489     """
490     if isinstance(test, unittest.TestCase):
491         yield test
492         return
493
494     subtests = list(test)
495     # custom test suite (no test cases)
496     if not len(subtests):
497         yield test
498         return
499
500     for item in itertools.chain.from_iterable(
501             itertools.imap(unwrap_suite, subtests)):
502         yield item
503
504 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: