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(module_name, f, path, descr)
140 sys.modules[module_part] = mod
141 sys.modules['openerp.addons.' + module_part] = mod
144 def initialize_sys_path():
145 """ Add all addons paths in sys.path.
147 This ensures something like ``import crm`` works even if the addons are
148 not in the PYTHONPATH.
154 ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(','))
155 ad_paths.append(_ad) # for get_module_path
156 sys.meta_path.append(AddonsImportHook())
158 def get_module_path(module, downloaded=False, display_warning=True):
159 """Return the path of the given module.
161 Search the addons paths and return the first path where the given
162 module is found. If downloaded is True, return the default addons
163 path if nothing else is found.
166 initialize_sys_path()
168 if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
169 return opj(adp, module)
172 return opj(_ad, module)
174 logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,))
178 def get_module_filetree(module, dir='.'):
179 path = get_module_path(module)
183 dir = os.path.normpath(dir)
186 if dir.startswith('..') or (dir and dir[0] == '/'):
187 raise Exception('Cannot access file outside the module')
189 if not os.path.isdir(path):
191 zip = zipfile.ZipFile(path + ".zip")
192 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
194 files = osutil.listdir(path, True)
198 if not f.startswith(dir):
202 f = f[len(dir)+int(not dir.endswith('/')):]
203 lst = f.split(os.sep)
206 current = current.setdefault(lst.pop(0), {})
207 current[lst.pop(0)] = None
211 def zip_directory(directory, b64enc=True, src=True):
212 """Compress a directory
214 @param directory: The directory to compress
215 @param base64enc: if True the function will encode the zip file with base64
216 @param src: Integrate the source files
218 @return: a string containing the zip file
221 RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
223 def _zippy(archive, path, src=True):
224 path = os.path.abspath(path)
225 base = os.path.basename(path)
226 for f in osutil.listdir(path, True):
227 bf = os.path.basename(f)
228 if not RE_exclude.search(bf) and (src or bf in ('__openerp__.py', '__terp__.py') or not bf.endswith('.py')):
229 archive.write(os.path.join(path, f), os.path.join(base, f))
231 archname = StringIO()
232 archive = PyZipFile(archname, "w", ZIP_DEFLATED)
234 # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8)
235 directory = tools.ustr(directory).encode('utf-8')
237 archive.writepy(directory)
238 _zippy(archive, directory, src=src)
240 archive_data = archname.getvalue()
244 return base64.encodestring(archive_data)
248 def get_module_as_zip(modulename, b64enc=True, src=True):
249 """Generate a module as zip file with the source or not and can do a base64 encoding
251 @param modulename: The module name
252 @param b64enc: if True the function will encode the zip file with base64
253 @param src: Integrate the source files
255 @return: a stream to store in a file-like object
258 ap = get_module_path(str(modulename))
260 raise Exception('Unable to find path for module %s' % modulename)
262 ap = ap.encode('utf8')
263 if os.path.isfile(ap + '.zip'):
264 val = file(ap + '.zip', 'rb').read()
266 val = base64.encodestring(val)
268 val = zip_directory(ap, b64enc, src)
273 def get_module_resource(module, *args):
274 """Return the full path of a resource of the given module.
276 @param module: the module
277 @param args: the resource path components
279 @return: absolute path to the resource
281 TODO name it get_resource_path
282 TODO make it available inside on osv object (self.get_resource_path)
284 a = get_module_path(module)
285 if not a: return False
286 resource_path = opj(a, *args)
287 if zipfile.is_zipfile( a +'.zip') :
288 zip = zipfile.ZipFile( a + ".zip")
289 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
290 resource_path = '/'.join(args)
291 if resource_path in files:
292 return opj(a, resource_path)
293 elif os.path.exists(resource_path):
297 def get_module_icon(module):
298 iconpath = ['static', 'src', 'img', 'icon.png']
299 if get_module_resource(module, *iconpath):
300 return ('/' + module + '/') + '/'.join(iconpath)
301 return '/base/' + '/'.join(iconpath)
303 def load_information_from_description_file(module):
305 :param module: The name of the module (sale, purchase, ...)
308 terp_file = get_module_resource(module, '__openerp__.py')
310 terp_file = get_module_resource(module, '__terp__.py')
311 mod_path = get_module_path(module)
314 if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
315 # default values for descriptor
318 'application': False,
320 'category': 'Uncategorized',
322 'complexity': 'normal',
325 'icon': get_module_icon(module),
335 info.update(itertools.izip(
336 'depends data demo test init_xml update_xml demo_xml'.split(),
339 with tools.file_open(terp_file) as terp_f:
340 info.update(eval(terp_f.read()))
344 #TODO: refactor the logger in this file to follow the logging guidelines
346 logging.getLogger('modules').debug('module %s: no descriptor file'
347 ' found: __openerp__.py or __terp__.py (deprecated)', module)
351 def init_module_models(cr, module_name, obj_list):
352 """ Initialize a list of models.
354 Call _auto_init and init on each model to create or update the
355 database tables supporting the models.
357 TODO better explanation of _auto_init and init.
360 logger.notifyChannel('init', netsvc.LOG_INFO,
361 'module %s: creating or updating database tables' % module_name)
364 result = obj._auto_init(cr, {'module': module_name})
367 if hasattr(obj, 'init'):
371 obj._auto_end(cr, {'module': module_name})
378 def register_module_classes(m):
379 """ Register module named m, if not already registered.
381 This loads the module and register all of its models, thanks to either
382 the MetaModel metaclass, or the explicit instantiation of the model.
387 mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
388 msg = "Couldn't load %smodule %s" % (mt, m)
389 logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg)
390 logger.notifyChannel('init', netsvc.LOG_CRITICAL, e)
395 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m)
396 mod_path = get_module_path(m)
398 initialize_sys_path()
400 zip_mod_path = mod_path + '.zip'
401 if not os.path.isfile(zip_mod_path):
402 __import__('openerp.addons.' + m)
404 zimp = zipimport.zipimporter(zip_mod_path)
414 """Returns the list of module names
418 name = os.path.basename(name)
419 if name[-4:] == '.zip':
423 def is_really_module(name):
424 name = opj(dir, name)
425 return os.path.isdir(name) or zipfile.is_zipfile(name)
426 return map(clean, filter(is_really_module, os.listdir(dir)))
429 initialize_sys_path()
431 plist.extend(listdir(ad))
432 return list(set(plist))
435 def get_modules_with_version():
436 modules = get_modules()
438 for module in modules:
440 info = load_information_from_description_file(module)
441 res[module] = "%s.%s" % (release.major_version, info['version'])
447 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: