# -*- coding: utf-8 -*-
# Copyright (C) 2017, Wolfgang Scherer, <Wolfgang.Scherer at gmx.de>
#
# This file is part of DOGgy Style Programming.
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
'''Sub-Module name import with clean namespace for ``from ... import *``.
Source code excerpt
===================
See also `source code of __init__.py`_.
Define package name and sub-modules to be imported (avoiding error-prone duplication)::
_package_ = 'flat_export'
_modules_ = ['sub1', 'sub2', 'sub3']
Use relative imports when available (this is imperative, see :func:`is_importing_package`)::
_loaded = False
if is_importing_package(_package_, locals()):
for _module in _modules_:
exec ('from .' + _module + ' import *')
_loaded = True
del(_module)
| Try importing the package, including `__all__`.
| This happens when executing a module file as script with the
package in the search path (e.g. ``python
flat_export/__init__.py``)
::
if not _loaded:
try:
exec('from ' + _package_ + ' import *')
exec('from ' + _package_ + ' import __all__')
_loaded = True
except (ImportError):
pass
| As a last resort, try importing the sub-modules directly.
| This happens when executing a module file as script inside the
package directory without the package in the search path
(e.g. ``cd flat_export; python __init__.py``)
::
if not _loaded:
for _module in _modules_:
exec('from ' + _module + ' import *')
del(_module)
Construct `__all__` (leaving out modules), unless it has been imported
before::
if not __all__:
_module_type = type(__import__('sys'))
for _sym, _val in sorted(locals().items()):
if not _sym.startswith('_') and not isinstance(_val, _module_type) :
__all__.append(_sym)
del(_sym)
del(_val)
del(_module_type)
.. _`source code of __init__.py`: _modules/flat_export.html#sub1_func
.. _`is_importing_package`: _modules/flat_export.html#is_importing_package
Import mode `direct` -- triggered by script execution
=====================================================
When
- the package is not installed
- the package is not otherwise in the search path
- the file `__init__.py` is executed as script::
cd flat_export
python __init__.py
Assuming this, the `_import_mode_` should be `direct`:
>>> _import_mode_
'direct'
and :func:`sub2_func` should be available:
>>> sub2_func()
# sub2 !
:func:`sub1_func` should be overwritten by sub-module :mod:`sub1`:
>>> sub1_func()
# sub1 !
Initially The namespace is still pretty clean (python3 extra keys
removed):
>>> print('\\n'.join(sorted(k for k in locals().keys() if k not in '__cached__ __loader__ __spec__ __warningregistry__'.split())))
__all__
__builtins__
__doc__
__file__
__name__
__package__
_import_mode_
_modules_
_package_
sub1_func
sub2_func
sub3_func
But as soon, as you start doing stuff ...:
>>> import os
>>> import sys
>>> from os import unlink
... things become messier:
>>> print('\\n'.join(sorted(k for k in locals().keys() if k not in '__cached__ __loader__ __spec__ __warningregistry__'.split())))
__all__
__builtins__
__doc__
__file__
__name__
__package__
_import_mode_
_modules_
_package_
os
sub1_func
sub2_func
sub3_func
sys
unlink
but `__all__` is still clean:
>>> print('\\n'.join(__all__))
sub1_func
sub2_func
sub3_func
.. note:: locals() and globals() are identical, when the module is
imported normally. They may be different if executed via `exec`
with a separate parameter for `locals`.
Prepare for tests outside package directory
-------------------------------------------
>>> import sys
>>> import os
Remove modules loaded by `direct` import:
>>> del(sys.modules['sub1'])
>>> del(sys.modules['sub2'])
>>> del(sys.modules['sub3'])
Remove script directory from `sys.path`:
>>> sys.path = [_p for _p in sys.path if _p != os.path.abspath(os.path.dirname(__file__))]
Add parent directory (as current directory) to path for module import:
>>> sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)))
Set current directory to parent directory of package directory:
>>> os.chdir(sys.path[0])
Setup namespace for next test (emulating empty script):
>>> for _name in list(locals().keys()):
... if _name not in ['__builtins__', '__doc__', '__file__', '__name__', '__package__', '_name']:
... del(locals()[_name])
>>> del(_name)
Import mode `internal` -- triggered by normal import
====================================================
Executing in the package parent directory means, that the package is
available for standard import.
Here are the currently defined names (python3 extra keys removed):
>>> print('\\n'.join(sorted(k for k in locals().keys() if k not in '__cached__ __loader__ __spec__ __warningregistry__'.split())))
__builtins__
__doc__
__file__
__name__
__package__
After a standard import of all symbols ...:
>>> from flat_export import *
... the additionally defined names `sub1_func`, `sub2_func`, `sub3_func` ...:
>>> print('\\n'.join(sorted(k for k in locals().keys() if k not in '__cached__ __loader__ __spec__ __warningregistry__'.split())))
__builtins__
__doc__
__file__
__name__
__package__
sub1_func
sub2_func
sub3_func
... are exactly what was intended:
>>> import flat_export
>>> flat_export.__all__
['sub1_func', 'sub2_func', 'sub3_func']
The sub-module imports were relative ...:
>>> flat_export._import_mode_
'internal'
... and the function `sub1_func` is overwritten by sub-module :mod:`sub1`:
>>> sub1_func()
# sub1 !
Prepare for exec test
---------------------
Setup namespace for next test (emulating empty script):
>>> import sys
>>> del(sys.modules['flat_export'])
>>> del(sys.modules['flat_export.sub1'])
>>> del(sys.modules['flat_export.sub2'])
>>> del(sys.modules['flat_export.sub3'])
>>> for _name in list(locals().keys()):
... if _name not in ['__builtins__', '__doc__', '__file__', '__name__', '__package__', '_name']:
... del(locals()[_name])
>>> del(_name)
Import mode `package` -- triggered by `exec`
============================================
In the package parent directory, define the module file and name in
`exec_locals_`:
>>> mod_file = 'flat_export/__init__.py'
>>> exec_locals_ = dict(
... __file__=mod_file,
... __name__='__init__')
Run via `exec`, with `__init__` as module name, collecting definitions
in `exec_locals_`:
>>> exec(compile(open(mod_file, "rb").read(), mod_file, 'exec'), globals(), exec_locals_ )
The imports happened via standard import from the executed code:
>>> exec_locals_['_import_mode_']
'package'
`__all__` is properly defined:
>>> exec_locals_['__all__']
['sub1_func', 'sub2_func', 'sub3_func']
The namespace is clean:
>>> print('\\n'.join(sorted(k for k in exec_locals_.keys() if k not in '__cached__ __loader__ __spec__ __warningregistry__'.split())))
__all__
__doc__
__file__
__name__
_import_mode_
_modules_
_package_
sub1_func
sub2_func
sub3_func
This module's locals are unaffected (python3 extra keys removed):
>>> print('\\n'.join(sorted(k for k in locals().keys() if k not in '__cached__ __loader__ __spec__ __warningregistry__'.split())))
__builtins__
__doc__
__file__
__name__
__package__
exec_locals_
mod_file
'''
def sub1_func():
"""Overwritten by sub module version."""
print('# __init__ !')
_package_ = 'flat_export'
_modules_ = ['sub1', 'sub2', 'sub3']
_import_mode_ = ''
__all__ = []
[docs]def is_importing_package(_package_, locals_, dummy_name=None):
""":returns: True, if relative package imports are working.
:param _package_: the package name (unfortunately, __package__
does not work, since it is None, when loading ``:(``).
:param locals_: module local variables for auto-removing function
after use.
:param dummy_name: dummy module name (default: 'dummy').
Tries to do a relative import from an empty module `.dummy`. This
avoids any secondary errors, other than::
ValueError: Attempted relative import in non-package
"""
success = False
if _package_:
import sys
dummy_name = dummy_name or 'dummy'
dummy_module = _package_ + '.' + dummy_name
if not dummy_module in sys.modules:
import imp
sys.modules[dummy_module] = imp.new_module(dummy_module)
try:
exec('from .' + dummy_name + ' import *')
success = True
except:
pass
if not 'sphinx.ext.autodoc' in __import__('sys').modules:
del(locals_['is_importing_package'])
return success
_loaded = False
if is_importing_package(_package_, locals()):
for _module in _modules_:
exec ('from .' + _module + ' import *')
_import_mode_ = 'internal'
_loaded = True
del(_module)
if not _loaded:
try:
exec('from ' + _package_ + ' import *')
exec('from ' + _package_ + ' import __all__')
_import_mode_ = 'package'
_loaded = True
except (ImportError):
pass
if not _loaded:
for _module in _modules_:
exec('from ' + _module + ' import *')
_import_mode_ = 'direct'
del(_module)
del(_loaded)
if not __all__:
_module_type = type(__import__('sys'))
for _sym, _val in sorted(locals().items()):
if not _sym.startswith('_') and not isinstance(_val, _module_type) :
__all__.append(_sym)
del(_sym)
del(_val)
del(_module_type)
# |:info:| the following raises an exception (_modules_ not
# defined) under python 3.5.2, when the import is triggered
# with an exec() call:
# __all__ = [_sym
# for _sym in sorted(locals().keys())
# if not _sym in _modules_ and not _sym.startswith('_')]
if _import_mode_ == 'internal' and False:
print('loading the package ...')
if __name__ == '__main__':
print('# flat_export doctest')
def _hidden_test(locals_):
del(locals_['_hidden_test'])
import doctest
doctest.testmod()
_hidden_test(locals())
print('_import_mode_: ' + str(_import_mode_))
# :ide: COMPILE: Run in parent dir with help(flat_export)
# . (progn (save-buffer) (compile (concat "cd .. && python -c 'import flat_export; help(flat_export)'")))
# :ide: COMPILE: Run in parent dir with python3 w/o args
# . (progn (save-buffer) (compile (concat "cd .. && python3 " (buffer-file-name) " ")))
# :ide: COMPILE: Runin parent dir w/o args
# . (progn (save-buffer) (compile (concat "cd .. && python " (buffer-file-name) " ")))
# :ide: COMPILE: Run with python3 w/o args
# . (progn (save-buffer) (compile (concat "python3 ./" (file-name-nondirectory (buffer-file-name)) " ")))
# :ide: COMPILE: Run w/o args
# . (progn (save-buffer) (compile (concat "python ./" (file-name-nondirectory (buffer-file-name)) " ")))