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 _ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base)
54 # Modules already loaded
57 logger = netsvc.Logger()
59 class AddonsImportHook(object):
61 Import hook to load OpenERP addons from multiple paths.
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`.
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
74 import openerp.addons.hr
75 loads the hr addons only once.
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.
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.
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.
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):
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')
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))
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.
113 def load_module(self, module_name):
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]
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]
127 # Check if the bare module name shadows another module.
128 f, path, descr = imp.find_module(module_part)
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.
136 # Note: we don't support circular import.
137 f, path, descr = imp.find_module(module_part, ad_paths)
138 mod = imp.load_module('openerp.addons.' + module_part, f, path, descr)
140 sys.modules[module_part] = mod
141 sys.modules['openerp.addons.' + module_part] = mod
144 def initialize_sys_path():
146 Setup an import-hook to be able to import OpenERP addons from the different
149 This ensures something like ``import crm`` (or even
150 ``import openerp.addons.crm``) works even if the addons are not in the
157 ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(','))
158 ad_paths.append(_ad) # for get_module_path
159 sys.meta_path.append(AddonsImportHook())
161 def get_module_path(module, downloaded=False, display_warning=True):
162 """Return the path of the given module.
164 Search the addons paths and return the first path where the given
165 module is found. If downloaded is True, return the default addons
166 path if nothing else is found.
169 initialize_sys_path()
171 if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
172 return opj(adp, module)
175 return opj(_ad, module)
177 logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,))
181 def get_module_filetree(module, dir='.'):
182 path = get_module_path(module)
186 dir = os.path.normpath(dir)
189 if dir.startswith('..') or (dir and dir[0] == '/'):
190 raise Exception('Cannot access file outside the module')
192 if not os.path.isdir(path):
194 zip = zipfile.ZipFile(path + ".zip")
195 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
197 files = osutil.listdir(path, True)
201 if not f.startswith(dir):
205 f = f[len(dir)+int(not dir.endswith('/')):]
206 lst = f.split(os.sep)
209 current = current.setdefault(lst.pop(0), {})
210 current[lst.pop(0)] = None
214 def zip_directory(directory, b64enc=True, src=True):
215 """Compress a directory
217 @param directory: The directory to compress
218 @param base64enc: if True the function will encode the zip file with base64
219 @param src: Integrate the source files
221 @return: a string containing the zip file
224 RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
226 def _zippy(archive, path, src=True):
227 path = os.path.abspath(path)
228 base = os.path.basename(path)
229 for f in osutil.listdir(path, True):
230 bf = os.path.basename(f)
231 if not RE_exclude.search(bf) and (src or bf in ('__openerp__.py', '__terp__.py') or not bf.endswith('.py')):
232 archive.write(os.path.join(path, f), os.path.join(base, f))
234 archname = StringIO()
235 archive = PyZipFile(archname, "w", ZIP_DEFLATED)
237 # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8)
238 directory = tools.ustr(directory).encode('utf-8')
240 archive.writepy(directory)
241 _zippy(archive, directory, src=src)
243 archive_data = archname.getvalue()
247 return base64.encodestring(archive_data)
251 def get_module_as_zip(modulename, b64enc=True, src=True):
252 """Generate a module as zip file with the source or not and can do a base64 encoding
254 @param modulename: The module name
255 @param b64enc: if True the function will encode the zip file with base64
256 @param src: Integrate the source files
258 @return: a stream to store in a file-like object
261 ap = get_module_path(str(modulename))
263 raise Exception('Unable to find path for module %s' % modulename)
265 ap = ap.encode('utf8')
266 if os.path.isfile(ap + '.zip'):
267 val = file(ap + '.zip', 'rb').read()
269 val = base64.encodestring(val)
271 val = zip_directory(ap, b64enc, src)
276 def get_module_resource(module, *args):
277 """Return the full path of a resource of the given module.
279 @param module: the module
280 @param args: the resource path components
282 @return: absolute path to the resource
284 TODO name it get_resource_path
285 TODO make it available inside on osv object (self.get_resource_path)
287 a = get_module_path(module)
288 if not a: return False
289 resource_path = opj(a, *args)
290 if zipfile.is_zipfile( a +'.zip') :
291 zip = zipfile.ZipFile( a + ".zip")
292 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
293 resource_path = '/'.join(args)
294 if resource_path in files:
295 return opj(a, resource_path)
296 elif os.path.exists(resource_path):
300 def get_module_icon(module):
301 iconpath = ['static', 'src', 'img', 'icon.png']
302 if get_module_resource(module, *iconpath):
303 return ('/' + module + '/') + '/'.join(iconpath)
304 return '/base/' + '/'.join(iconpath)
306 def load_information_from_description_file(module):
308 :param module: The name of the module (sale, purchase, ...)
311 terp_file = get_module_resource(module, '__openerp__.py')
313 terp_file = get_module_resource(module, '__terp__.py')
314 mod_path = get_module_path(module)
317 if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
318 # default values for descriptor
321 'application': False,
323 'category': 'Uncategorized',
325 'complexity': 'normal',
328 'icon': get_module_icon(module),
338 info.update(itertools.izip(
339 'depends data demo test init_xml update_xml demo_xml'.split(),
342 with tools.file_open(terp_file) as terp_f:
343 info.update(eval(terp_f.read()))
347 #TODO: refactor the logger in this file to follow the logging guidelines
349 logging.getLogger('modules').debug('module %s: no descriptor file'
350 ' found: __openerp__.py or __terp__.py (deprecated)', module)
354 def init_module_models(cr, module_name, obj_list):
355 """ Initialize a list of models.
357 Call _auto_init and init on each model to create or update the
358 database tables supporting the models.
360 TODO better explanation of _auto_init and init.
363 logger.notifyChannel('init', netsvc.LOG_INFO,
364 'module %s: creating or updating database tables' % module_name)
367 result = obj._auto_init(cr, {'module': module_name})
370 if hasattr(obj, 'init'):
374 obj._auto_end(cr, {'module': module_name})
381 def register_module_classes(m):
382 """ Register module named m, if not already registered.
384 This loads the module and register all of its models, thanks to either
385 the MetaModel metaclass, or the explicit instantiation of the model.
390 mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
391 msg = "Couldn't load %smodule %s" % (mt, m)
392 logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg)
393 logger.notifyChannel('init', netsvc.LOG_CRITICAL, e)
398 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m)
399 mod_path = get_module_path(m)
401 initialize_sys_path()
403 zip_mod_path = mod_path + '.zip'
404 if not os.path.isfile(zip_mod_path):
405 __import__('openerp.addons.' + m)
407 zimp = zipimport.zipimporter(zip_mod_path)
417 """Returns the list of module names
421 name = os.path.basename(name)
422 if name[-4:] == '.zip':
426 def is_really_module(name):
427 name = opj(dir, name)
428 return os.path.isdir(name) or zipfile.is_zipfile(name)
429 return map(clean, filter(is_really_module, os.listdir(dir)))
432 initialize_sys_path()
434 plist.extend(listdir(ad))
435 return list(set(plist))
438 def get_modules_with_version():
439 modules = get_modules()
441 for module in modules:
443 info = load_information_from_description_file(module)
444 res[module] = "%s.%s" % (release.major_version, info['version'])
450 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: