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