[IMP] post_load: factored common code. This also fix bug lp:929466
[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 os, sys, imp
24 from os.path import join as opj
25 import itertools
26 import zipimport
27
28 import openerp
29
30 import openerp.osv as osv
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 from openerp.tools.translate import _
35
36 import openerp.netsvc as netsvc
37
38 import zipfile
39 import openerp.release as release
40
41 import re
42 import base64
43 from zipfile import PyZipFile, ZIP_DEFLATED
44 from cStringIO import StringIO
45
46 import logging
47
48 import openerp.modules.db
49 import openerp.modules.graph
50
51 _logger = logging.getLogger(__name__)
52
53 _ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base)
54 ad_paths = []
55
56 # Modules already loaded
57 loaded = []
58
59 _logger = logging.getLogger(__name__)
60
61 class AddonsImportHook(object):
62     """
63     Import hook to load OpenERP addons from multiple paths.
64
65     OpenERP implements its own import-hook to load its addons. OpenERP
66     addons are Python modules. Originally, they were each living in their
67     own top-level namespace, e.g. the sale module, or the hr module. For
68     backward compatibility, `import <module>` is still supported. Now they
69     are living in `openerp.addons`. The good way to import such modules is
70     thus `import openerp.addons.module`.
71
72     For backward compatibility, loading an addons puts it in `sys.modules`
73     under both the legacy (short) name, and the new (longer) name. This
74     ensures that
75         import hr
76         import openerp.addons.hr
77     loads the hr addons only once.
78
79     When an OpenERP addons name clashes with some other installed Python
80     module (for instance this is the case of the `resource` addons),
81     obtaining the OpenERP addons is only possible with the long name. The
82     short name will give the expected Python module.
83
84     Instead of relying on some addons path, an alternative approach would be
85     to use pkg_resources entry points from already installed Python libraries
86     (and install our addons as such). Even when implemented, we would still
87     have to support the addons path approach for backward compatibility.
88     """
89
90     def find_module(self, module_name, package_path):
91         module_parts = module_name.split('.')
92         if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
93             return self # We act as a loader too.
94
95         # TODO list of loadable modules can be cached instead of always
96         # calling get_module_path().
97         if len(module_parts) == 1 and \
98             get_module_path(module_parts[0],
99                 display_warning=False):
100             try:
101                 # Check if the bare module name clashes with another module.
102                 f, path, descr = imp.find_module(module_parts[0])
103                 _logger.warning("""
104 Ambiguous import: the OpenERP module `%s` is shadowed by another
105 module (available at %s).
106 To import it, use `import openerp.addons.<module>.`.""" % (module_name, path))
107                 return
108             except ImportError, e:
109                 # Using `import <module_name>` instead of
110                 # `import openerp.addons.<module_name>` is ugly but not harmful
111                 # and kept for backward compatibility.
112                 return self # We act as a loader too.
113
114     def load_module(self, module_name):
115
116         module_parts = module_name.split('.')
117         if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
118             module_part = module_parts[2]
119             if module_name in sys.modules:
120                 return sys.modules[module_name]
121
122         if len(module_parts) == 1:
123             module_part = module_parts[0]
124             if module_part in sys.modules:
125                 return sys.modules[module_part]
126
127         try:
128             # Check if the bare module name shadows another module.
129             f, path, descr = imp.find_module(module_part)
130             is_shadowing = True
131         except ImportError, e:
132             # Using `import <module_name>` instead of
133             # `import openerp.addons.<module_name>` is ugly but not harmful
134             # and kept for backward compatibility.
135             is_shadowing = False
136
137         # Note: we don't support circular import.
138         f, path, descr = imp.find_module(module_part, ad_paths)
139         mod = imp.load_module('openerp.addons.' + module_part, f, path, descr)
140         if not is_shadowing:
141             sys.modules[module_part] = mod
142             for k in sys.modules.keys():
143                 if k.startswith('openerp.addons.' + module_part):
144                     sys.modules[k[len('openerp.addons.'):]] = sys.modules[k]
145         sys.modules['openerp.addons.' + module_part] = mod
146         return mod
147
148 def initialize_sys_path():
149     """
150     Setup an import-hook to be able to import OpenERP addons from the different
151     addons paths.
152
153     This ensures something like ``import crm`` (or even
154     ``import openerp.addons.crm``) works even if the addons are not in the
155     PYTHONPATH.
156     """
157     global ad_paths
158     if ad_paths:
159         return
160
161     ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(','))
162     ad_paths.append(_ad) # for get_module_path
163     sys.meta_path.append(AddonsImportHook())
164
165 def get_module_path(module, downloaded=False, display_warning=True):
166     """Return the path of the given module.
167
168     Search the addons paths and return the first path where the given
169     module is found. If downloaded is True, return the default addons
170     path if nothing else is found.
171
172     """
173     initialize_sys_path()
174     for adp in ad_paths:
175         if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
176             return opj(adp, module)
177
178     if downloaded:
179         return opj(_ad, module)
180     if display_warning:
181         _logger.warning('module %s: module not found', module)
182     return False
183
184
185 def get_module_filetree(module, dir='.'):
186     path = get_module_path(module)
187     if not path:
188         return False
189
190     dir = os.path.normpath(dir)
191     if dir == '.':
192         dir = ''
193     if dir.startswith('..') or (dir and dir[0] == '/'):
194         raise Exception('Cannot access file outside the module')
195
196     if not os.path.isdir(path):
197         # zipmodule
198         zip = zipfile.ZipFile(path + ".zip")
199         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
200     else:
201         files = osutil.listdir(path, True)
202
203     tree = {}
204     for f in files:
205         if not f.startswith(dir):
206             continue
207
208         if dir:
209             f = f[len(dir)+int(not dir.endswith('/')):]
210         lst = f.split(os.sep)
211         current = tree
212         while len(lst) != 1:
213             current = current.setdefault(lst.pop(0), {})
214         current[lst.pop(0)] = None
215
216     return tree
217
218 def zip_directory(directory, b64enc=True, src=True):
219     """Compress a directory
220
221     @param directory: The directory to compress
222     @param base64enc: if True the function will encode the zip file with base64
223     @param src: Integrate the source files
224
225     @return: a string containing the zip file
226     """
227
228     RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
229
230     def _zippy(archive, path, src=True):
231         path = os.path.abspath(path)
232         base = os.path.basename(path)
233         for f in osutil.listdir(path, True):
234             bf = os.path.basename(f)
235             if not RE_exclude.search(bf) and (src or bf in ('__openerp__.py', '__terp__.py') or not bf.endswith('.py')):
236                 archive.write(os.path.join(path, f), os.path.join(base, f))
237
238     archname = StringIO()
239     archive = PyZipFile(archname, "w", ZIP_DEFLATED)
240
241     # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8)
242     directory = tools.ustr(directory).encode('utf-8')
243
244     archive.writepy(directory)
245     _zippy(archive, directory, src=src)
246     archive.close()
247     archive_data = archname.getvalue()
248     archname.close()
249
250     if b64enc:
251         return base64.encodestring(archive_data)
252
253     return archive_data
254
255 def get_module_as_zip(modulename, b64enc=True, src=True):
256     """Generate a module as zip file with the source or not and can do a base64 encoding
257
258     @param modulename: The module name
259     @param b64enc: if True the function will encode the zip file with base64
260     @param src: Integrate the source files
261
262     @return: a stream to store in a file-like object
263     """
264
265     ap = get_module_path(str(modulename))
266     if not ap:
267         raise Exception('Unable to find path for module %s' % modulename)
268
269     ap = ap.encode('utf8')
270     if os.path.isfile(ap + '.zip'):
271         val = file(ap + '.zip', 'rb').read()
272         if b64enc:
273             val = base64.encodestring(val)
274     else:
275         val = zip_directory(ap, b64enc, src)
276
277     return val
278
279
280 def get_module_resource(module, *args):
281     """Return the full path of a resource of the given module.
282
283     @param module: the module
284     @param args: the resource path components
285
286     @return: absolute path to the resource
287
288     TODO name it get_resource_path
289     TODO make it available inside on osv object (self.get_resource_path)
290     """
291     a = get_module_path(module)
292     if not a: return False
293     resource_path = opj(a, *args)
294     if zipfile.is_zipfile( a +'.zip') :
295         zip = zipfile.ZipFile( a + ".zip")
296         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
297         resource_path = '/'.join(args)
298         if resource_path in files:
299             return opj(a, resource_path)
300     elif os.path.exists(resource_path):
301         return resource_path
302     return False
303
304 def get_module_icon(module):
305     iconpath = ['static', 'src', 'img', 'icon.png']
306     if get_module_resource(module, *iconpath):
307         return ('/' + module + '/') + '/'.join(iconpath)
308     return '/base/'  + '/'.join(iconpath)
309
310 def load_information_from_description_file(module):
311     """
312     :param module: The name of the module (sale, purchase, ...)
313     """
314
315     terp_file = get_module_resource(module, '__openerp__.py')
316     if not terp_file:
317         terp_file = get_module_resource(module, '__terp__.py')
318     mod_path = get_module_path(module)
319     if terp_file:
320         info = {}
321         if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
322             # default values for descriptor
323             info = {
324                 'application': False,
325                 'author': '',
326                 'auto_install': False,
327                 'category': 'Uncategorized',
328                 'certificate': None,
329                 'complexity': 'normal',
330                 'depends': [],
331                 'description': '',
332                 'icon': get_module_icon(module),
333                 'installable': True,
334                 'auto_install': False,
335                 'license': 'AGPL-3',
336                 'name': False,
337                 'post_load': None,
338                 'version': '0.0.0',
339                 'web': False,
340                 'website': '',
341                 'sequence': 100,
342             }
343             info.update(itertools.izip(
344                 'depends data demo test init_xml update_xml demo_xml'.split(),
345                 iter(list, None)))
346
347             f = tools.file_open(terp_file)
348             try:
349                 info.update(eval(f.read()))
350             finally:
351                 f.close()
352
353             if 'active' in info:
354                 # 'active' has been renamed 'auto_install'
355                 info['auto_install'] = info['active']
356
357             return info
358
359     #TODO: refactor the logger in this file to follow the logging guidelines
360     #      for 6.0
361     _logger.debug('module %s: no descriptor file'
362         ' found: __openerp__.py or __terp__.py (deprecated)', module)
363     return {}
364
365
366 def init_module_models(cr, module_name, obj_list):
367     """ Initialize a list of models.
368
369     Call _auto_init and init on each model to create or update the
370     database tables supporting the models.
371
372     TODO better explanation of _auto_init and init.
373
374     """
375     _logger.info('module %s: creating or updating database tables', module_name)
376     todo = []
377     for obj in obj_list:
378         result = obj._auto_init(cr, {'module': module_name})
379         if result:
380             todo += result
381         if hasattr(obj, 'init'):
382             obj.init(cr)
383         cr.commit()
384     for obj in obj_list:
385         obj._auto_end(cr, {'module': module_name})
386         cr.commit()
387     todo.sort()
388     for t in todo:
389         t[1](cr, *t[2])
390     cr.commit()
391
392 def load_openerp_module(module_name):
393     """ Load an OpenERP module, if not already loaded.
394
395     This loads the module and register all of its models, thanks to either
396     the MetaModel metaclass, or the explicit instantiation of the model.
397     This is also used to load server-wide module (i.e. it is also used
398     when there is no model to register).
399     """
400     global loaded
401     if module_name in loaded:
402         return
403
404     initialize_sys_path()
405     try:
406         mod_path = get_module_path(module_name)
407         zip_mod_path = mod_path + '.zip'
408         if not os.path.isfile(zip_mod_path):
409             __import__('openerp.addons.' + module_name)
410         else:
411             zimp = zipimport.zipimporter(zip_mod_path)
412             zimp.load_module(module_name)
413
414         # Call the module's post-load hook. This can done before any model or
415         # data has been initialized. This is ok as the post-load hook is for
416         # server-wide (instead of registry-specific) functionalities.
417         info = load_information_from_description_file(module_name)
418         if info['post_load']:
419             getattr(sys.modules['openerp.addons.' + module_name], info['post_load'])()
420
421     except Exception, e:
422         mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
423         msg = "Couldn't load %smodule %s" % (mt, module_name)
424         _logger.critical(msg)
425         _logger.critical(e)
426         raise
427     else:
428         loaded.append(module_name)
429
430 def get_modules():
431     """Returns the list of module names
432     """
433     def listdir(dir):
434         def clean(name):
435             name = os.path.basename(name)
436             if name[-4:] == '.zip':
437                 name = name[:-4]
438             return name
439
440         def is_really_module(name):
441             name = opj(dir, name)
442             return os.path.isdir(name) or zipfile.is_zipfile(name)
443         return map(clean, filter(is_really_module, os.listdir(dir)))
444
445     plist = []
446     initialize_sys_path()
447     for ad in ad_paths:
448         plist.extend(listdir(ad))
449     return list(set(plist))
450
451
452 def get_modules_with_version():
453     modules = get_modules()
454     res = {}
455     for module in modules:
456         try:
457             info = load_information_from_description_file(module)
458             res[module] = "%s.%s" % (release.major_version, info['version'])
459         except Exception, e:
460             continue
461     return res
462
463
464 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: