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