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 = logging.getLogger(__name__)
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])
104 Ambiguous import: the OpenERP module `%s` is shadowed by another
105 module (available at %s).
106 To import it, use `import openerp.addons.<module>.`.""" % (module_name, path))
108 except ImportError, e:
109 # Using `import <module_name>` instead of
110 # `import openerp.addons.<module_name>` is ugly but not harmful
111 # and kept for backward compatibility.
112 return self # We act as a loader too.
114 def load_module(self, module_name):
116 module_parts = module_name.split('.')
117 if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
118 module_part = module_parts[2]
119 if module_name in sys.modules:
120 return sys.modules[module_name]
122 if len(module_parts) == 1:
123 module_part = module_parts[0]
124 if module_part in sys.modules:
125 return sys.modules[module_part]
128 # Check if the bare module name shadows another module.
129 f, path, descr = imp.find_module(module_part)
131 except ImportError, e:
132 # Using `import <module_name>` instead of
133 # `import openerp.addons.<module_name>` is ugly but not harmful
134 # and kept for backward compatibility.
137 # Note: we don't support circular import.
138 f, path, descr = imp.find_module(module_part, ad_paths)
139 mod = imp.load_module('openerp.addons.' + module_part, f, path, descr)
141 sys.modules[module_part] = mod
142 for k in sys.modules.keys():
143 if k.startswith('openerp.addons.' + module_part):
144 sys.modules[k[len('openerp.addons.'):]] = sys.modules[k]
145 sys.modules['openerp.addons.' + module_part] = mod
148 def initialize_sys_path():
150 Setup an import-hook to be able to import OpenERP addons from the different
153 This ensures something like ``import crm`` (or even
154 ``import openerp.addons.crm``) works even if the addons are not in the
161 ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(','))
162 ad_paths.append(_ad) # for get_module_path
163 sys.meta_path.append(AddonsImportHook())
165 def get_module_path(module, downloaded=False, display_warning=True):
166 """Return the path of the given module.
168 Search the addons paths and return the first path where the given
169 module is found. If downloaded is True, return the default addons
170 path if nothing else is found.
173 initialize_sys_path()
175 if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
176 return opj(adp, module)
179 return opj(_ad, module)
181 _logger.warning('module %s: module not found', module)
185 def get_module_filetree(module, dir='.'):
186 path = get_module_path(module)
190 dir = os.path.normpath(dir)
193 if dir.startswith('..') or (dir and dir[0] == '/'):
194 raise Exception('Cannot access file outside the module')
196 if not os.path.isdir(path):
198 zip = zipfile.ZipFile(path + ".zip")
199 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
201 files = osutil.listdir(path, True)
205 if not f.startswith(dir):
209 f = f[len(dir)+int(not dir.endswith('/')):]
210 lst = f.split(os.sep)
213 current = current.setdefault(lst.pop(0), {})
214 current[lst.pop(0)] = None
218 def zip_directory(directory, b64enc=True, src=True):
219 """Compress a directory
221 @param directory: The directory to compress
222 @param base64enc: if True the function will encode the zip file with base64
223 @param src: Integrate the source files
225 @return: a string containing the zip file
228 RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
230 def _zippy(archive, path, src=True):
231 path = os.path.abspath(path)
232 base = os.path.basename(path)
233 for f in osutil.listdir(path, True):
234 bf = os.path.basename(f)
235 if not RE_exclude.search(bf) and (src or bf in ('__openerp__.py', '__terp__.py') or not bf.endswith('.py')):
236 archive.write(os.path.join(path, f), os.path.join(base, f))
238 archname = StringIO()
239 archive = PyZipFile(archname, "w", ZIP_DEFLATED)
241 # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8)
242 directory = tools.ustr(directory).encode('utf-8')
244 archive.writepy(directory)
245 _zippy(archive, directory, src=src)
247 archive_data = archname.getvalue()
251 return base64.encodestring(archive_data)
255 def get_module_as_zip(modulename, b64enc=True, src=True):
256 """Generate a module as zip file with the source or not and can do a base64 encoding
258 @param modulename: The module name
259 @param b64enc: if True the function will encode the zip file with base64
260 @param src: Integrate the source files
262 @return: a stream to store in a file-like object
265 ap = get_module_path(str(modulename))
267 raise Exception('Unable to find path for module %s' % modulename)
269 ap = ap.encode('utf8')
270 if os.path.isfile(ap + '.zip'):
271 val = file(ap + '.zip', 'rb').read()
273 val = base64.encodestring(val)
275 val = zip_directory(ap, b64enc, src)
280 def get_module_resource(module, *args):
281 """Return the full path of a resource of the given module.
283 @param module: the module
284 @param args: the resource path components
286 @return: absolute path to the resource
288 TODO name it get_resource_path
289 TODO make it available inside on osv object (self.get_resource_path)
291 a = get_module_path(module)
292 if not a: return False
293 resource_path = opj(a, *args)
294 if zipfile.is_zipfile( a +'.zip') :
295 zip = zipfile.ZipFile( a + ".zip")
296 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
297 resource_path = '/'.join(args)
298 if resource_path in files:
299 return opj(a, resource_path)
300 elif os.path.exists(resource_path):
304 def get_module_icon(module):
305 iconpath = ['static', 'src', 'img', 'icon.png']
306 if get_module_resource(module, *iconpath):
307 return ('/' + module + '/') + '/'.join(iconpath)
308 return '/base/' + '/'.join(iconpath)
310 def load_information_from_description_file(module):
312 :param module: The name of the module (sale, purchase, ...)
315 terp_file = get_module_resource(module, '__openerp__.py')
317 terp_file = get_module_resource(module, '__terp__.py')
318 mod_path = get_module_path(module)
321 if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
322 # default values for descriptor
324 'application': False,
326 'auto_install': False,
327 'category': 'Uncategorized',
329 'complexity': 'normal',
332 'icon': get_module_icon(module),
334 'auto_install': False,
343 info.update(itertools.izip(
344 'depends data demo test init_xml update_xml demo_xml'.split(),
347 f = tools.file_open(terp_file)
349 info.update(eval(f.read()))
354 # 'active' has been renamed 'auto_install'
355 info['auto_install'] = info['active']
359 #TODO: refactor the logger in this file to follow the logging guidelines
361 _logger.debug('module %s: no descriptor file'
362 ' found: __openerp__.py or __terp__.py (deprecated)', module)
366 def init_module_models(cr, module_name, obj_list):
367 """ Initialize a list of models.
369 Call _auto_init and init on each model to create or update the
370 database tables supporting the models.
372 TODO better explanation of _auto_init and init.
375 _logger.info('module %s: creating or updating database tables', module_name)
378 result = obj._auto_init(cr, {'module': module_name})
381 if hasattr(obj, 'init'):
385 obj._auto_end(cr, {'module': module_name})
392 def load_openerp_module(module_name):
393 """ Load an OpenERP module, if not already loaded.
395 This loads the module and register all of its models, thanks to either
396 the MetaModel metaclass, or the explicit instantiation of the model.
397 This is also used to load server-wide module (i.e. it is also used
398 when there is no model to register).
401 if module_name in loaded:
404 initialize_sys_path()
406 mod_path = get_module_path(module_name)
407 zip_mod_path = mod_path + '.zip'
408 if not os.path.isfile(zip_mod_path):
409 __import__('openerp.addons.' + module_name)
411 zimp = zipimport.zipimporter(zip_mod_path)
412 zimp.load_module(module_name)
414 # Call the module's post-load hook. This can done before any model or
415 # data has been initialized. This is ok as the post-load hook is for
416 # server-wide (instead of registry-specific) functionalities.
417 info = load_information_from_description_file(module_name)
418 if info['post_load']:
419 getattr(sys.modules['openerp.addons.' + module_name], info['post_load'])()
422 mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
423 msg = "Couldn't load %smodule %s" % (mt, module_name)
424 _logger.critical(msg)
428 loaded.append(module_name)
431 """Returns the list of module names
435 name = os.path.basename(name)
436 if name[-4:] == '.zip':
440 def is_really_module(name):
441 name = opj(dir, name)
442 return os.path.isdir(name) or zipfile.is_zipfile(name)
443 return map(clean, filter(is_really_module, os.listdir(dir)))
446 initialize_sys_path()
448 plist.extend(listdir(ad))
449 return list(set(plist))
452 def get_modules_with_version():
453 modules = get_modules()
455 for module in modules:
457 info = load_information_from_description_file(module)
458 res[module] = "%s.%s" % (release.major_version, info['version'])
464 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: