[IMP] import hook: moved to a proper location (openerp.modules.module instead of...
[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(module_name, 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     """ Add all addons paths in sys.path.
146
147     This ensures something like ``import crm`` works even if the addons are
148     not in the PYTHONPATH.
149     """
150     global ad_paths
151     if ad_paths:
152         return
153
154     ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(','))
155     ad_paths.append(_ad) # for get_module_path
156     sys.meta_path.append(AddonsImportHook())
157
158 def get_module_path(module, downloaded=False, display_warning=True):
159     """Return the path of the given module.
160
161     Search the addons paths and return the first path where the given
162     module is found. If downloaded is True, return the default addons
163     path if nothing else is found.
164
165     """
166     initialize_sys_path()
167     for adp in ad_paths:
168         if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
169             return opj(adp, module)
170
171     if downloaded:
172         return opj(_ad, module)
173     if display_warning:
174         logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,))
175     return False
176
177
178 def get_module_filetree(module, dir='.'):
179     path = get_module_path(module)
180     if not path:
181         return False
182
183     dir = os.path.normpath(dir)
184     if dir == '.':
185         dir = ''
186     if dir.startswith('..') or (dir and dir[0] == '/'):
187         raise Exception('Cannot access file outside the module')
188
189     if not os.path.isdir(path):
190         # zipmodule
191         zip = zipfile.ZipFile(path + ".zip")
192         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
193     else:
194         files = osutil.listdir(path, True)
195
196     tree = {}
197     for f in files:
198         if not f.startswith(dir):
199             continue
200
201         if dir:
202             f = f[len(dir)+int(not dir.endswith('/')):]
203         lst = f.split(os.sep)
204         current = tree
205         while len(lst) != 1:
206             current = current.setdefault(lst.pop(0), {})
207         current[lst.pop(0)] = None
208
209     return tree
210
211 def zip_directory(directory, b64enc=True, src=True):
212     """Compress a directory
213
214     @param directory: The directory to compress
215     @param base64enc: if True the function will encode the zip file with base64
216     @param src: Integrate the source files
217
218     @return: a string containing the zip file
219     """
220
221     RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
222
223     def _zippy(archive, path, src=True):
224         path = os.path.abspath(path)
225         base = os.path.basename(path)
226         for f in osutil.listdir(path, True):
227             bf = os.path.basename(f)
228             if not RE_exclude.search(bf) and (src or bf in ('__openerp__.py', '__terp__.py') or not bf.endswith('.py')):
229                 archive.write(os.path.join(path, f), os.path.join(base, f))
230
231     archname = StringIO()
232     archive = PyZipFile(archname, "w", ZIP_DEFLATED)
233
234     # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8)
235     directory = tools.ustr(directory).encode('utf-8')
236
237     archive.writepy(directory)
238     _zippy(archive, directory, src=src)
239     archive.close()
240     archive_data = archname.getvalue()
241     archname.close()
242
243     if b64enc:
244         return base64.encodestring(archive_data)
245
246     return archive_data
247
248 def get_module_as_zip(modulename, b64enc=True, src=True):
249     """Generate a module as zip file with the source or not and can do a base64 encoding
250
251     @param modulename: The module name
252     @param b64enc: if True the function will encode the zip file with base64
253     @param src: Integrate the source files
254
255     @return: a stream to store in a file-like object
256     """
257
258     ap = get_module_path(str(modulename))
259     if not ap:
260         raise Exception('Unable to find path for module %s' % modulename)
261
262     ap = ap.encode('utf8')
263     if os.path.isfile(ap + '.zip'):
264         val = file(ap + '.zip', 'rb').read()
265         if b64enc:
266             val = base64.encodestring(val)
267     else:
268         val = zip_directory(ap, b64enc, src)
269
270     return val
271
272
273 def get_module_resource(module, *args):
274     """Return the full path of a resource of the given module.
275
276     @param module: the module
277     @param args: the resource path components
278
279     @return: absolute path to the resource
280
281     TODO name it get_resource_path
282     TODO make it available inside on osv object (self.get_resource_path)
283     """
284     a = get_module_path(module)
285     if not a: return False
286     resource_path = opj(a, *args)
287     if zipfile.is_zipfile( a +'.zip') :
288         zip = zipfile.ZipFile( a + ".zip")
289         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
290         resource_path = '/'.join(args)
291         if resource_path in files:
292             return opj(a, resource_path)
293     elif os.path.exists(resource_path):
294         return resource_path
295     return False
296
297 def get_module_icon(module):
298     iconpath = ['static', 'src', 'img', 'icon.png']
299     if get_module_resource(module, *iconpath):
300         return ('/' + module + '/') + '/'.join(iconpath)
301     return '/base/'  + '/'.join(iconpath)
302
303 def load_information_from_description_file(module):
304     """
305     :param module: The name of the module (sale, purchase, ...)
306     """
307
308     terp_file = get_module_resource(module, '__openerp__.py')
309     if not terp_file:
310         terp_file = get_module_resource(module, '__terp__.py')
311     mod_path = get_module_path(module)
312     if terp_file:
313         info = {}
314         if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
315             # default values for descriptor
316             info = {
317                 'active': False,
318                 'application': False,
319                 'author': '',
320                 'category': 'Uncategorized',
321                 'certificate': None,
322                 'complexity': 'normal',
323                 'depends': [],
324                 'description': '',
325                 'icon': get_module_icon(module),
326                 'installable': True,
327                 'license': 'AGPL-3',
328                 'name': False,
329                 'post_load': None,
330                 'version': '0.0.0',
331                 'web': False,
332                 'website': '',
333                 'sequence': 100,
334             }
335             info.update(itertools.izip(
336                 'depends data demo test init_xml update_xml demo_xml'.split(),
337                 iter(list, None)))
338
339             with tools.file_open(terp_file) as terp_f:
340                 info.update(eval(terp_f.read()))
341
342             return info
343
344     #TODO: refactor the logger in this file to follow the logging guidelines
345     #      for 6.0
346     logging.getLogger('modules').debug('module %s: no descriptor file'
347         ' found: __openerp__.py or __terp__.py (deprecated)', module)
348     return {}
349
350
351 def init_module_models(cr, module_name, obj_list):
352     """ Initialize a list of models.
353
354     Call _auto_init and init on each model to create or update the
355     database tables supporting the models.
356
357     TODO better explanation of _auto_init and init.
358
359     """
360     logger.notifyChannel('init', netsvc.LOG_INFO,
361         'module %s: creating or updating database tables' % module_name)
362     todo = []
363     for obj in obj_list:
364         result = obj._auto_init(cr, {'module': module_name})
365         if result:
366             todo += result
367         if hasattr(obj, 'init'):
368             obj.init(cr)
369         cr.commit()
370     for obj in obj_list:
371         obj._auto_end(cr, {'module': module_name})
372         cr.commit()
373     todo.sort()
374     for t in todo:
375         t[1](cr, *t[2])
376     cr.commit()
377
378 def register_module_classes(m):
379     """ Register module named m, if not already registered.
380
381     This loads the module and register all of its models, thanks to either
382     the MetaModel metaclass, or the explicit instantiation of the model.
383
384     """
385
386     def log(e):
387         mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
388         msg = "Couldn't load %smodule %s" % (mt, m)
389         logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg)
390         logger.notifyChannel('init', netsvc.LOG_CRITICAL, e)
391
392     global loaded
393     if m in loaded:
394         return
395     logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m)
396     mod_path = get_module_path(m)
397
398     initialize_sys_path()
399     try:
400         zip_mod_path = mod_path + '.zip'
401         if not os.path.isfile(zip_mod_path):
402             __import__('openerp.addons.' + m)
403         else:
404             zimp = zipimport.zipimporter(zip_mod_path)
405             zimp.load_module(m)
406     except Exception, e:
407         log(e)
408         raise
409     else:
410         loaded.append(m)
411
412
413 def get_modules():
414     """Returns the list of module names
415     """
416     def listdir(dir):
417         def clean(name):
418             name = os.path.basename(name)
419             if name[-4:] == '.zip':
420                 name = name[:-4]
421             return name
422
423         def is_really_module(name):
424             name = opj(dir, name)
425             return os.path.isdir(name) or zipfile.is_zipfile(name)
426         return map(clean, filter(is_really_module, os.listdir(dir)))
427
428     plist = []
429     initialize_sys_path()
430     for ad in ad_paths:
431         plist.extend(listdir(ad))
432     return list(set(plist))
433
434
435 def get_modules_with_version():
436     modules = get_modules()
437     res = {}
438     for module in modules:
439         try:
440             info = load_information_from_description_file(module)
441             res[module] = "%s.%s" % (release.major_version, info['version'])
442         except Exception, e:
443             continue
444     return res
445
446
447 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: