[IMP] module: use the `openerp.addons.` namespace to load addons.
[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 def initialize_sys_path():
60     """ Add all addons paths in sys.path.
61
62     This ensures something like ``import crm`` works even if the addons are
63     not in the PYTHONPATH.
64     """
65     global ad_paths
66     if ad_paths:
67         return
68
69     ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(','))
70     ad_paths.append(_ad) # for get_module_path
71
72
73 def get_module_path(module, downloaded=False, display_warning=True):
74     """Return the path of the given module.
75
76     Search the addons paths and return the first path where the given
77     module is found. If downloaded is True, return the default addons
78     path if nothing else is found.
79
80     """
81     initialize_sys_path()
82     for adp in ad_paths:
83         if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
84             return opj(adp, module)
85
86     if downloaded:
87         return opj(_ad, module)
88     if display_warning:
89         logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,))
90     return False
91
92
93 def get_module_filetree(module, dir='.'):
94     path = get_module_path(module)
95     if not path:
96         return False
97
98     dir = os.path.normpath(dir)
99     if dir == '.':
100         dir = ''
101     if dir.startswith('..') or (dir and dir[0] == '/'):
102         raise Exception('Cannot access file outside the module')
103
104     if not os.path.isdir(path):
105         # zipmodule
106         zip = zipfile.ZipFile(path + ".zip")
107         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
108     else:
109         files = osutil.listdir(path, True)
110
111     tree = {}
112     for f in files:
113         if not f.startswith(dir):
114             continue
115
116         if dir:
117             f = f[len(dir)+int(not dir.endswith('/')):]
118         lst = f.split(os.sep)
119         current = tree
120         while len(lst) != 1:
121             current = current.setdefault(lst.pop(0), {})
122         current[lst.pop(0)] = None
123
124     return tree
125
126 def zip_directory(directory, b64enc=True, src=True):
127     """Compress a directory
128
129     @param directory: The directory to compress
130     @param base64enc: if True the function will encode the zip file with base64
131     @param src: Integrate the source files
132
133     @return: a string containing the zip file
134     """
135
136     RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
137
138     def _zippy(archive, path, src=True):
139         path = os.path.abspath(path)
140         base = os.path.basename(path)
141         for f in osutil.listdir(path, True):
142             bf = os.path.basename(f)
143             if not RE_exclude.search(bf) and (src or bf in ('__openerp__.py', '__terp__.py') or not bf.endswith('.py')):
144                 archive.write(os.path.join(path, f), os.path.join(base, f))
145
146     archname = StringIO()
147     archive = PyZipFile(archname, "w", ZIP_DEFLATED)
148
149     # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8)
150     directory = tools.ustr(directory).encode('utf-8')
151
152     archive.writepy(directory)
153     _zippy(archive, directory, src=src)
154     archive.close()
155     archive_data = archname.getvalue()
156     archname.close()
157
158     if b64enc:
159         return base64.encodestring(archive_data)
160
161     return archive_data
162
163 def get_module_as_zip(modulename, b64enc=True, src=True):
164     """Generate a module as zip file with the source or not and can do a base64 encoding
165
166     @param modulename: The module name
167     @param b64enc: if True the function will encode the zip file with base64
168     @param src: Integrate the source files
169
170     @return: a stream to store in a file-like object
171     """
172
173     ap = get_module_path(str(modulename))
174     if not ap:
175         raise Exception('Unable to find path for module %s' % modulename)
176
177     ap = ap.encode('utf8')
178     if os.path.isfile(ap + '.zip'):
179         val = file(ap + '.zip', 'rb').read()
180         if b64enc:
181             val = base64.encodestring(val)
182     else:
183         val = zip_directory(ap, b64enc, src)
184
185     return val
186
187
188 def get_module_resource(module, *args):
189     """Return the full path of a resource of the given module.
190
191     @param module: the module
192     @param args: the resource path components
193
194     @return: absolute path to the resource
195
196     TODO name it get_resource_path
197     TODO make it available inside on osv object (self.get_resource_path)
198     """
199     a = get_module_path(module)
200     if not a: return False
201     resource_path = opj(a, *args)
202     if zipfile.is_zipfile( a +'.zip') :
203         zip = zipfile.ZipFile( a + ".zip")
204         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
205         resource_path = '/'.join(args)
206         if resource_path in files:
207             return opj(a, resource_path)
208     elif os.path.exists(resource_path):
209         return resource_path
210     return False
211
212 def get_module_icon(module):
213     iconpath = ['static', 'src', 'img', 'icon.png']
214     if get_module_resource(module, *iconpath):
215         return ('/' + module + '/') + '/'.join(iconpath)
216     return '/base/'  + '/'.join(iconpath)
217
218 def load_information_from_description_file(module):
219     """
220     :param module: The name of the module (sale, purchase, ...)
221     """
222
223     terp_file = get_module_resource(module, '__openerp__.py')
224     if not terp_file:
225         terp_file = get_module_resource(module, '__terp__.py')
226     mod_path = get_module_path(module)
227     if terp_file:
228         info = {}
229         if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
230             # default values for descriptor
231             info = {
232                 'active': False,
233                 'application': False,
234                 'author': '',
235                 'category': 'Uncategorized',
236                 'certificate': None,
237                 'complexity': 'normal',
238                 'depends': [],
239                 'description': '',
240                 'icon': get_module_icon(module),
241                 'installable': True,
242                 'license': 'AGPL-3',
243                 'name': False,
244                 'post_load': None,
245                 'version': '0.0.0',
246                 'web': False,
247                 'website': '',
248                 'sequence': 100,
249             }
250             info.update(itertools.izip(
251                 'depends data demo test init_xml update_xml demo_xml'.split(),
252                 iter(list, None)))
253
254             with tools.file_open(terp_file) as terp_f:
255                 info.update(eval(terp_f.read()))
256
257             return info
258
259     #TODO: refactor the logger in this file to follow the logging guidelines
260     #      for 6.0
261     logging.getLogger('modules').debug('module %s: no descriptor file'
262         ' found: __openerp__.py or __terp__.py (deprecated)', module)
263     return {}
264
265
266 def init_module_models(cr, module_name, obj_list):
267     """ Initialize a list of models.
268
269     Call _auto_init and init on each model to create or update the
270     database tables supporting the models.
271
272     TODO better explanation of _auto_init and init.
273
274     """
275     logger.notifyChannel('init', netsvc.LOG_INFO,
276         'module %s: creating or updating database tables' % module_name)
277     todo = []
278     for obj in obj_list:
279         result = obj._auto_init(cr, {'module': module_name})
280         if result:
281             todo += result
282         if hasattr(obj, 'init'):
283             obj.init(cr)
284         cr.commit()
285     for obj in obj_list:
286         obj._auto_end(cr, {'module': module_name})
287         cr.commit()
288     todo.sort()
289     for t in todo:
290         t[1](cr, *t[2])
291     cr.commit()
292
293 def register_module_classes(m):
294     """ Register module named m, if not already registered.
295
296     This loads the module and register all of its models, thanks to either
297     the MetaModel metaclass, or the explicit instantiation of the model.
298
299     """
300
301     def log(e):
302         mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
303         msg = "Couldn't load %smodule %s" % (mt, m)
304         logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg)
305         logger.notifyChannel('init', netsvc.LOG_CRITICAL, e)
306
307     global loaded
308     if m in loaded:
309         return
310     logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m)
311     mod_path = get_module_path(m)
312
313     initialize_sys_path()
314     try:
315         zip_mod_path = mod_path + '.zip'
316         if not os.path.isfile(zip_mod_path):
317             __import__('openerp.modules.' + m)
318         else:
319             zimp = zipimport.zipimporter(zip_mod_path)
320             zimp.load_module(m)
321     except Exception, e:
322         log(e)
323         raise
324     else:
325         loaded.append(m)
326
327
328 def get_modules():
329     """Returns the list of module names
330     """
331     def listdir(dir):
332         def clean(name):
333             name = os.path.basename(name)
334             if name[-4:] == '.zip':
335                 name = name[:-4]
336             return name
337
338         def is_really_module(name):
339             name = opj(dir, name)
340             return os.path.isdir(name) or zipfile.is_zipfile(name)
341         return map(clean, filter(is_really_module, os.listdir(dir)))
342
343     plist = []
344     initialize_sys_path()
345     for ad in ad_paths:
346         plist.extend(listdir(ad))
347     return list(set(plist))
348
349
350 def get_modules_with_version():
351     modules = get_modules()
352     res = {}
353     for module in modules:
354         try:
355             info = load_information_from_description_file(module)
356             res[module] = "%s.%s" % (release.major_version, info['version'])
357         except Exception, e:
358             continue
359     return res
360
361
362 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: