[MERGE] use an import hook instead of appending to sys.path.

This replaces sys.path mutation with an import hook. It removes the clash 
between our own 'resource' addons and the standard Python resource module. It
provides a transition between our current practice and a cleaner
'openerp.addons' namespace.

The import hook makes it possible to either 'import openerp.addons.hr' or 
'import hr' in the addons (ie. both paths are set in sys.modules). When trying 
to import an already existing (non-openerp) Python module (e.g. resource), it
will indeed load the Python module and print a warning (to invite you to use
openerp.addons.resource if this is the one you want).

Unfortunately, the import hook doesn't support self-referential imports. E.g. 
hr.wizard.xxx can't use 'import hr.yyy' or 'from hr import yyy'. Python will
fail to load hr with cryptic error messages or even a segmentation fault.

Beginning with this patch, 'import openerp.addons.xxx' is the way to go, 
'import xxx' is deprecated. Once 'import xxx' support can be removed, we will
probably support pkgutil or setuptool namespaces.

bzr revid: vmt@openerp.com-20120118104505-tns626ga47dwl7it
This commit is contained in:
Vo Minh Thu
2012-01-18 11:45:05 +01:00
5 changed files with 115 additions and 37 deletions
+4
View File
@@ -42,6 +42,9 @@ import openerp
__author__ = openerp.release.author
__version__ = openerp.release.version
import sys
import imp
def check_root_user():
""" Exit if the process's user is 'root' (on POSIX system)."""
if os.name == 'posix':
@@ -212,6 +215,7 @@ if __name__ == "__main__":
check_root_user()
openerp.tools.config.parse_config(sys.argv[1:])
check_postgres_user()
openerp.netsvc.init_logger()
report_configuration()
+3 -3
View File
@@ -25,9 +25,9 @@
This module serves to contain all OpenERP addons, across all configured addons
paths. For the code to manage those addons, see openerp.modules.
Addons are made available here (i.e. under openerp.addons) after
openerp.tools.config.parse_config() is called (so that the addons paths
are known).
Addons are made available under `openerp.addons` after
openerp.tools.config.parse_config() is called (so that the addons paths are
known).
This module also conveniently reexports some symbols from openerp.modules.
Importing them from here is deprecated.
+98 -30
View File
@@ -56,31 +56,109 @@ loaded = []
logger = netsvc.Logger()
def initialize_sys_path():
""" Add all addons paths in sys.path.
class AddonsImportHook(object):
"""
Import hook to load OpenERP addons from multiple paths.
This ensures something like ``import crm`` works even if the addons are
not in the PYTHONPATH.
OpenERP implements its own import-hook to load its addons. OpenERP
addons are Python modules. Originally, they were each living in their
own top-level namespace, e.g. the sale module, or the hr module. For
backward compatibility, `import <module>` is still supported. Now they
are living in `openerp.addons`. The good way to import such modules is
thus `import openerp.addons.module`.
For backward compatibility, loading an addons puts it in `sys.modules`
under both the legacy (short) name, and the new (longer) name. This
ensures that
import hr
import openerp.addons.hr
loads the hr addons only once.
When an OpenERP addons name clashes with some other installed Python
module (for instance this is the case of the `resource` addons),
obtaining the OpenERP addons is only possible with the long name. The
short name will give the expected Python module.
Instead of relying on some addons path, an alternative approach would be
to use pkg_resources entry points from already installed Python libraries
(and install our addons as such). Even when implemented, we would still
have to support the addons path approach for backward compatibility.
"""
def find_module(self, module_name, package_path):
module_parts = module_name.split('.')
if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
return self # We act as a loader too.
# TODO list of loadable modules can be cached instead of always
# calling get_module_path().
if len(module_parts) == 1 and \
get_module_path(module_parts[0],
display_warning=False):
try:
# Check if the bare module name clashes with another module.
f, path, descr = imp.find_module(module_parts[0])
logger = logging.getLogger('init')
logger.warning("""
Ambiguous import: the OpenERP module `%s` is shadowed by another
module (available at %s).
To import it, use `import openerp.addons.<module>.`.""" % (module_name, path))
return
except ImportError, e:
# Using `import <module_name>` instead of
# `import openerp.addons.<module_name>` is ugly but not harmful
# and kept for backward compatibility.
return self # We act as a loader too.
def load_module(self, module_name):
module_parts = module_name.split('.')
if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
module_part = module_parts[2]
if module_name in sys.modules:
return sys.modules[module_name]
if len(module_parts) == 1:
module_part = module_parts[0]
if module_part in sys.modules:
return sys.modules[module_part]
try:
# Check if the bare module name shadows another module.
f, path, descr = imp.find_module(module_part)
is_shadowing = True
except ImportError, e:
# Using `import <module_name>` instead of
# `import openerp.addons.<module_name>` is ugly but not harmful
# and kept for backward compatibility.
is_shadowing = False
# Note: we don't support circular import.
f, path, descr = imp.find_module(module_part, ad_paths)
mod = imp.load_module(module_name, f, path, descr)
if not is_shadowing:
sys.modules[module_part] = mod
sys.modules['openerp.addons.' + module_part] = mod
return mod
def initialize_sys_path():
"""
Setup an import-hook to be able to import OpenERP addons from the different
addons paths.
This ensures something like ``import crm`` (or even
``import openerp.addons.crm``) works even if the addons are not in the
PYTHONPATH.
"""
global ad_paths
if ad_paths:
return
ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(','))
ad_paths.append(_ad) # for get_module_path
sys.meta_path.append(AddonsImportHook())
sys.path.insert(1, _ad)
ad_cnt=1
for adp in ad_paths:
if adp != _ad:
sys.path.insert(ad_cnt, adp)
ad_cnt+=1
ad_paths.append(_ad) # for get_module_path
def get_module_path(module, downloaded=False):
def get_module_path(module, downloaded=False, display_warning=True):
"""Return the path of the given module.
Search the addons paths and return the first path where the given
@@ -95,7 +173,8 @@ def get_module_path(module, downloaded=False):
if downloaded:
return opj(_ad, module)
logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,))
if display_warning:
logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,))
return False
@@ -299,17 +378,6 @@ def init_module_models(cr, module_name, obj_list):
t[1](cr, *t[2])
cr.commit()
# Import hook to write a addon m in both sys.modules['m'] and
# sys.modules['openerp.addons.m']. Otherwise it could be loaded twice
# if imported twice using different names.
#class MyImportHook(object):
# def find_module(self, module_name, package_path):
# print ">>>", module_name, package_path
# def load_module(self, module_name):
# raise ImportError("Restricted")
#sys.meta_path.append(MyImportHook())
def register_module_classes(m):
""" Register module named m, if not already registered.
@@ -334,7 +402,7 @@ def register_module_classes(m):
try:
zip_mod_path = mod_path + '.zip'
if not os.path.isfile(zip_mod_path):
__import__(m)
__import__('openerp.addons.' + m)
else:
zimp = zipimport.zipimporter(zip_mod_path)
zimp.load_module(m)
+10 -1
View File
@@ -608,7 +608,16 @@ class MetaModel(type):
super(MetaModel, self).__init__(name, bases, attrs)
return
module_name = self.__module__.split('.')[0]
# The (OpenERP) module name can be in the `openerp.addons` namespace
# or not. For instance module `sale` can be imported as
# `openerp.addons.sale` (the good way) or `sale` (for backward
# compatibility).
module_parts = self.__module__.split('.')
if len(module_parts) > 2 and module_parts[0] == 'openerp' and \
module_parts[1] == 'addons':
module_name = self.__module__.split('.')[2]
else:
module_name = self.__module__.split('.')[0]
if not hasattr(self, '_module'):
self._module = module_name
-3
View File
@@ -461,9 +461,6 @@ class configmanager(object):
if complete:
openerp.modules.module.initialize_sys_path()
openerp.modules.loading.open_openerp_namespace()
# openerp.addons.__path__.extend(openerp.conf.addons_paths) # This
# is not compatible with initialize_sys_path(): import crm and
# import openerp.addons.crm load twice the module.
def _generate_pgpassfile(self):
"""