Source code for flat_export

# -*- 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)) " ")))