1 # -*- coding: utf-8 -*-
2 ##############################################################################
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>).
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.
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.
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/>.
21 ##############################################################################
24 from os.path import join as opj
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 _
36 import openerp.netsvc as netsvc
39 import openerp.release as release
43 from zipfile import PyZipFile, ZIP_DEFLATED
44 from cStringIO import StringIO
48 import openerp.modules.db
49 import openerp.modules.graph
51 _logger = logging.getLogger(__name__)
53 _ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base)
56 # Modules already loaded
59 logger = netsvc.Logger()
61 class AddonsImportHook(object):
63 Import hook to load OpenERP addons from multiple paths.
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`.
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
76 import openerp.addons.hr
77 loads the hr addons only once.
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.
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.
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.
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):
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')
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))
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.
115 def load_module(self, module_name):
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]
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]
129 # Check if the bare module name shadows another module.
130 f, path, descr = imp.find_module(module_part)
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.
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)
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
149 def initialize_sys_path():
151 Setup an import-hook to be able to import OpenERP addons from the different
154 This ensures something like ``import crm`` (or even
155 ``import openerp.addons.crm``) works even if the addons are not in the
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())
166 def get_module_path(module, downloaded=False, display_warning=True):
167 """Return the path of the given module.
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.
174 initialize_sys_path()
176 if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
177 return opj(adp, module)
180 return opj(_ad, module)
182 logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,))
186 def get_module_filetree(module, dir='.'):
187 path = get_module_path(module)
191 dir = os.path.normpath(dir)
194 if dir.startswith('..') or (dir and dir[0] == '/'):
195 raise Exception('Cannot access file outside the module')
197 if not os.path.isdir(path):
199 zip = zipfile.ZipFile(path + ".zip")
200 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
202 files = osutil.listdir(path, True)
206 if not f.startswith(dir):
210 f = f[len(dir)+int(not dir.endswith('/')):]
211 lst = f.split(os.sep)
214 current = current.setdefault(lst.pop(0), {})
215 current[lst.pop(0)] = None
219 def zip_directory(directory, b64enc=True, src=True):
220 """Compress a directory
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
226 @return: a string containing the zip file
229 RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
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))
239 archname = StringIO()
240 archive = PyZipFile(archname, "w", ZIP_DEFLATED)
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')
245 archive.writepy(directory)
246 _zippy(archive, directory, src=src)
248 archive_data = archname.getvalue()
252 return base64.encodestring(archive_data)
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
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
263 @return: a stream to store in a file-like object
266 ap = get_module_path(str(modulename))
268 raise Exception('Unable to find path for module %s' % modulename)
270 ap = ap.encode('utf8')
271 if os.path.isfile(ap + '.zip'):
272 val = file(ap + '.zip', 'rb').read()
274 val = base64.encodestring(val)
276 val = zip_directory(ap, b64enc, src)
281 def get_module_resource(module, *args):
282 """Return the full path of a resource of the given module.
284 @param module: the module
285 @param args: the resource path components
287 @return: absolute path to the resource
289 TODO name it get_resource_path
290 TODO make it available inside on osv object (self.get_resource_path)
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):
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)
311 def load_information_from_description_file(module):
313 :param module: The name of the module (sale, purchase, ...)
316 terp_file = get_module_resource(module, '__openerp__.py')
318 terp_file = get_module_resource(module, '__terp__.py')
319 mod_path = get_module_path(module)
322 if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
323 # default values for descriptor
325 'application': False,
327 'auto_install': False,
328 'category': 'Uncategorized',
330 'complexity': 'normal',
333 'icon': get_module_icon(module),
335 'auto_install': False,
344 info.update(itertools.izip(
345 'depends data demo test init_xml update_xml demo_xml'.split(),
348 f = tools.file_open(terp_file)
350 info.update(eval(f.read()))
355 # 'active' has been renamed 'auto_install'
356 info['auto_install'] = info['active']
360 #TODO: refactor the logger in this file to follow the logging guidelines
362 logging.getLogger('modules').debug('module %s: no descriptor file'
363 ' found: __openerp__.py or __terp__.py (deprecated)', module)
367 def init_module_models(cr, module_name, obj_list):
368 """ Initialize a list of models.
370 Call _auto_init and init on each model to create or update the
371 database tables supporting the models.
373 TODO better explanation of _auto_init and init.
376 logger.notifyChannel('init', netsvc.LOG_INFO,
377 'module %s: creating or updating database tables' % module_name)
380 result = obj._auto_init(cr, {'module': module_name})
383 if hasattr(obj, 'init'):
387 obj._auto_end(cr, {'module': module_name})
394 def register_module_classes(m):
395 """ Register module named m, if not already registered.
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.
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)
411 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m)
412 mod_path = get_module_path(m)
414 initialize_sys_path()
416 zip_mod_path = mod_path + '.zip'
417 if not os.path.isfile(zip_mod_path):
418 __import__('openerp.addons.' + m)
420 zimp = zipimport.zipimporter(zip_mod_path)
430 """Returns the list of module names
434 name = os.path.basename(name)
435 if name[-4:] == '.zip':
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)))
445 initialize_sys_path()
447 plist.extend(listdir(ad))
448 return list(set(plist))
451 def get_modules_with_version():
452 modules = get_modules()
454 for module in modules:
456 info = load_information_from_description_file(module)
457 res[module] = "%s.%s" % (release.major_version, info['version'])
463 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: