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