commit 6dfe84bae366c56f269504d0b1058bae9ba5be50 Author: chfw Date: Tue Apr 4 08:53:11 2017 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4727a58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,95 @@ +# April 2016 +# reference: https://github.com/github/gitignore/blob/master/Python.gitignore +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject +*~ +commons/ +commons +.moban.hashes diff --git a/.moban.d/README.rst.jj2 b/.moban.d/README.rst.jj2 new file mode 100644 index 0000000..f9516d4 --- /dev/null +++ b/.moban.d/README.rst.jj2 @@ -0,0 +1,33 @@ +================================================================================ +{{name}} +================================================================================ + +.. image:: https://api.travis-ci.org/{{organisation}}/{{name}}.svg?branch=master + :target: http://travis-ci.org/{{organisation}}/{{name}} + +.. image:: https://codecov.io/github/{{organisation}}/{{name}}/coverage.png + :target: https://codecov.io/github/{{organisation}}/{{name}} + +{%block documentation_link%} +.. image:: https://readthedocs.org/projects/{{name|lower}}/badge/?version=latest + :target: http://{{name|lower}}.readthedocs.org/en/latest/ +{%endblock%} + + +Installation +================================================================================ + +You can install it via pip: + +.. code-block:: bash + + $ pip install {{name}} + + +or clone it and install it: + +.. code-block:: bash + + $ git clone http://github.com/{{organisation}}/{{name}}.git + $ cd {{name}} + $ python setup.py install diff --git a/.moban.d/docs/source/conf.py.jj2 b/.moban.d/docs/source/conf.py.jj2 new file mode 100644 index 0000000..ecb9725 --- /dev/null +++ b/.moban.d/docs/source/conf.py.jj2 @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +{%block additional_imports%} +{%endblock%} +DESCRIPTION = ( +{% for i in range(0, description|length, 70) %} + '{{ description[i:(70+i)] }}' + +{% endfor %} + '' +) +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', +{%block SPHINX_EXTENSIONS%} +{%endblock%} +] + +templates_path = ['_templates'] +source_suffix = '.rst' +master_doc = 'index' + +project = u'{{name}}' +copyright = u'{{copyright_year}} {{company}}' +version = '{{release}}' +release = '{{version}}' +exclude_patterns = [] +pygments_style = 'sphinx' +{%block custom_doc_theme%} +html_theme = 'default' +{%endblock%} +html_static_path = ['_static'] +htmlhelp_basename = '{{name}}doc' +latex_elements = {} +latex_documents = [ + ('index', '{{name}}.tex', + '{{name}} Documentation', + '{{company}}', 'manual'), +] +man_pages = [ + ('index', '{{name}}', + '{{name}} Documentation', + [u'{{company}}'], 1) +] +texinfo_documents = [ + ('index', '{{name}}', + '{{name}} Documentation', + '{{company}}', '{{name}}', + DESCRIPTION, + 'Miscellaneous'), +] diff --git a/.moban.d/requirements.txt.jj2 b/.moban.d/requirements.txt.jj2 new file mode 100644 index 0000000..8337402 --- /dev/null +++ b/.moban.d/requirements.txt.jj2 @@ -0,0 +1,3 @@ +{% for dependency in dependencies: %} +{{dependency}} +{% endfor %} diff --git a/.moban.d/setup.py.jj2 b/.moban.d/setup.py.jj2 new file mode 100644 index 0000000..19227cd --- /dev/null +++ b/.moban.d/setup.py.jj2 @@ -0,0 +1,172 @@ +{% if external_module_library %} +from distutils.core import setup, Extension +{% else %} +try: + from setuptools import setup, find_packages +except ImportError: + from ez_setup import use_setuptools + use_setuptools() + from setuptools import setup, find_packages +{%endif%} +{%block platform_block%} +from platform import python_implementation +{%endblock%} +{%block compat_block%} +import sys +PY2 = sys.version_info[0] == 2 +PY26 = PY2 and sys.version_info[1] < 7 +{%endblock%} + +NAME = '{{name}}' +AUTHOR = '{{author}}' +VERSION = '{{version}}' +EMAIL = '{{contact}}' +LICENSE = '{{license}}' +{% if command_line_interface %} +ENTRY_POINTS = { + 'console_scripts': [ + '{{command_line_interface}} = {{ entry_point }}' + ] +} +{% endif %} +DESCRIPTION = ( +{% for i in range(0, description|length, 70) %} + '{{ description[i:(70+i)] }}' + +{% endfor %} + '' +) +KEYWORDS = [ + {%block additional_keywords%} + {%endblock%} +] + +CLASSIFIERS = [ + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + {%block additional_classifiers%} + {%endblock %} +] + +{%macro handle_complex_dependency(complex_one) -%} +{%set dependency, condition = complex_one.split(';')%} +{% if condition == 'python_version<"3"'%} +if PY2: +{% endif %} +{% if condition == 'python_version>="3"'%} +if not PY2: +{% endif %} +{% if condition == 'python_version<"2.7"'%} +if PY26: +{% endif %} +{% if condition == 'platform_python_implementation=="PyPy"'%} +if python_implementation == "PyPy": +{%endif%} + INSTALL_REQUIRES.append('{{dependency}}') +{%- endmacro %} +INSTALL_REQUIRES = [ +{% for dependency in dependencies: %} + {% if ';' not in dependency: %} + '{{dependency}}', + {% endif %} +{% endfor %} +] + +{% for dependency in dependencies: %} + {% if ';' in dependency: %} +{{handle_complex_dependency(dependency)}} + {% endif %} +{% endfor %} + +{% if external_module_library %} +PYMODULE = Extension( + '{{name}}', + sources=[ +{% for source in sources: %} + '{{source}}', +{% endfor %} + ], + libraries=INSTALL_REQUIRES +) +{% else %} +PACKAGES = find_packages(exclude=['ez_setup', 'examples', 'tests']) +{% if extra_dependencies or dependencies: %} +EXTRAS_REQUIRE = { + {% for dependency in extra_dependencies: %} + {% for key, value in dependency.items(): %} + '{{key}}': {{value}}, + {% endfor %} + {% endfor %} +} +{% else: %} +EXTRAS_REQUIRE = {} +{% endif %} +{% endif %} + + +def read_files(*files): + """Read files into setup""" + text = "" + for single_file in files: + content = read(single_file) + text = text + content + "\n" + return text + + +def read(afile): + """Read a file into setup""" + with open(afile, 'r') as opened_file: + content = filter_out_test_code(opened_file) + content = "".join(list(content)) + return content + + +def filter_out_test_code(file_handle): + found_test_code = False + for line in file_handle.readlines(): + if line.startswith('.. testcode:'): + found_test_code = True + continue + if found_test_code is True: + if line.startswith(' '): + continue + else: + empty_line = line.strip() + if len(empty_line) == 0: + continue + else: + found_test_code = False + yield line + else: + yield line + + +if __name__ == '__main__': + setup( + name=NAME, + author=AUTHOR, + version=VERSION, + author_email=EMAIL, + description=DESCRIPTION, + long_description=read_files('README.rst', 'CHANGELOG.rst'), + license=LICENSE, + keywords=KEYWORDS, +{% if external_module_library %} + ext_modules=[PYMODULE], +{% else %} + extras_require=EXTRAS_REQUIRE, + tests_require=['nose'], + install_requires=INSTALL_REQUIRES, + packages=PACKAGES, + include_package_data=True, + zip_safe=False, +{% if command_line_interface %} + entry_points=ENTRY_POINTS, +{% endif %} +{% endif%} + classifiers=CLASSIFIERS + ) diff --git a/.moban.d/test.sh.jj2 b/.moban.d/test.sh.jj2 new file mode 100644 index 0000000..7a2305e --- /dev/null +++ b/.moban.d/test.sh.jj2 @@ -0,0 +1,16 @@ +{%block pretest%} +{%endblock%} +{%if external_module_library%} + {%set package=external_module_library%} +{%else%} + {%if command_line_interface%} + {%set package=command_line_interface + '_cli' %} + {%else%} + {%set package=name%} + {%endif%} +{%endif%} +pip freeze +nosetests --with-cov --cover-package {{package|lower}} --cover-package tests {%if not exclude_doctest%}--with-doctest --doctest-extension=.rst README.rst{%endif%} tests {%if not nodocs%}docs/source{%endif%} {%if not external_module_library%}{{package|lower}}{%endif%} && flake8 . --exclude=.moban.d {%block flake8_options%}--builtins=unicode,xrange,long{%endblock%} + +{%block posttest%} +{%endblock%} diff --git a/.moban.d/tests/requirements.txt.jj2 b/.moban.d/tests/requirements.txt.jj2 new file mode 100644 index 0000000..69ed58e --- /dev/null +++ b/.moban.d/tests/requirements.txt.jj2 @@ -0,0 +1,6 @@ +nose +codecov +coverage +flake8 +{%block extras %} +{%endblock%} diff --git a/.moban.yml b/.moban.yml new file mode 100644 index 0000000..a815035 --- /dev/null +++ b/.moban.yml @@ -0,0 +1,13 @@ +configuration: + configuration_dir: "commons/config" + template_dir: + - "commons/templates" + - ".moban.d" + configuration: lml.yml +targets: + - README.rst: README.rst.jj2 + - setup.py: setup.py.jj2 + - requirements.txt: requirements.txt.jj2 + - "tests/requirements.txt": "tests/requirements.txt.jj2" + - "docs/source/conf.py": "docs/source/conf.py.jj2" + - test.sh: test.sh.jj2 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..93d2a73 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +sudo: false +language: python +notifications: + email: false +python: + - 3.6 + - 3.5 + - 3.4 + - 3.3 + - 2.7 + - 2.6 + - pypy +before_install: + - pip install -r tests/requirements.txt +script: + - make test +after_success: + codecov diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..7398c8c --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,2 @@ +Change log +=========== diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..5f13ef0 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include README.rst +include CHANGELOG.rst diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..26d9b68 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +all: test + +test: + bash test.sh + +document: + bash document.sh diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..412adc1 --- /dev/null +++ b/README.rst @@ -0,0 +1,31 @@ +================================================================================ +lml +================================================================================ + +.. image:: https://api.travis-ci.org/chfw/lml.svg?branch=master + :target: http://travis-ci.org/chfw/lml + +.. image:: https://codecov.io/github/chfw/lml/coverage.png + :target: https://codecov.io/github/chfw/lml + +.. image:: https://readthedocs.org/projects/lml/badge/?version=latest + :target: http://lml.readthedocs.org/en/latest/ + + +Installation +================================================================================ + +You can install it via pip: + +.. code-block:: bash + + $ pip install lml + + +or clone it and install it: + +.. code-block:: bash + + $ git clone http://github.com/chfw/lml.git + $ cd lml + $ python setup.py install diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..6634ad9 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +DESCRIPTION = ( + 'Plugin first, Load me later' + + '' +) +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', +] + +templates_path = ['_templates'] +source_suffix = '.rst' +master_doc = 'index' + +project = u'lml' +copyright = u'2017 Onni Software Ltd.' +version = '0.0.1' +release = '0.0.1' +exclude_patterns = [] +pygments_style = 'sphinx' +html_theme = 'default' +html_static_path = ['_static'] +htmlhelp_basename = 'lmldoc' +latex_elements = {} +latex_documents = [ + ('index', 'lml.tex', + 'lml Documentation', + 'Onni Software Ltd.', 'manual'), +] +man_pages = [ + ('index', 'lml', + 'lml Documentation', + [u'Onni Software Ltd.'], 1) +] +texinfo_documents = [ + ('index', 'lml', + 'lml Documentation', + 'Onni Software Ltd.', 'lml', + DESCRIPTION, + 'Miscellaneous'), +] diff --git a/lml.yml b/lml.yml new file mode 100644 index 0000000..50bc0da --- /dev/null +++ b/lml.yml @@ -0,0 +1,12 @@ +name: "lml" +organisation: "chfw" +author: "C.W." +contact: "wangc_2011(@)hotmail.com" +company: "Onni Software Ltd." +version: "0.0.1" +release: "0.0.1" +copyright_year: 2017 +license: New BSD +dependencies: + - six +description: "Plugin first, Load me later" \ No newline at end of file diff --git a/lml/__init__.py b/lml/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lml/_compact.py b/lml/_compact.py new file mode 100644 index 0000000..8ecb8b7 --- /dev/null +++ b/lml/_compact.py @@ -0,0 +1,20 @@ +def with_metaclass(meta, *bases): + # This requires a bit of explanation: the basic idea is to make a + # dummy metaclass for one level of class instantiation that replaces + # itself with the actual metaclass. Because of internal type checks + # we also need to make sure that we downgrade the custom metaclass + # for one level to something closer to type (that's why __call__ and + # __init__ comes back from type etc.). + # + # This has the advantage over six.with_metaclass in that it does not + # introduce dummy classes into the final MRO. + # :copyright: (c) 2014 by Armin Ronacher. + # :license: BSD, see LICENSE for more details. + class metaclass(meta): + __call__ = type.__call__ + __init__ = type.__init__ + def __new__(cls, name, this_bases, d): + if this_bases is None: + return type.__new__(cls, name, (), d) + return meta(name, bases, d) + return metaclass('temporary_class', None, {}) diff --git a/lml/manager.py b/lml/manager.py new file mode 100644 index 0000000..ca0cdc0 --- /dev/null +++ b/lml/manager.py @@ -0,0 +1,31 @@ +PLUG_IN_MANAGERS = {} + + +def register_class(cls): + PLUG_IN_MANAGERS[cls.name] = cls + + +class PluginManager(object): + name = "plugin" + + def plugin_first(self, meta, module_name): + pass + + def list_registry(self): + pass + + def load_me_later(self, registry_key): + pass + + def plugin_now(self, cls): + pass + + def get_plugin(self): + pass + + +def plugin_first(meta, module_name): + manager = PLUG_IN_MANAGERS.get(meta['plugin_type']) + manager.plugin_first(meta, module_name) + + diff --git a/lml/plugin.py b/lml/plugin.py new file mode 100644 index 0000000..a52e2be --- /dev/null +++ b/lml/plugin.py @@ -0,0 +1,42 @@ +import pkgutil +from itertools import chain +from lml.manager import plugin_first + + +def scan_plugins(prefix, marker, path, black_list): + # scan pkgutil.iter_modules + module_names = (module_info[1] for module_info in pkgutil.iter_modules() + if module_info[2] and module_info[1].startswith(prefix)) + + # scan pyinstaller + module_names_from_pyinstaller = scan_from_pyinstaller(prefix, path) + # loop through modules and find our plug ins + for module_name in chain(module_names, module_names_from_pyinstaller): + + if module_name in black_list: + continue + + try: + plugin = __import__(module_name) + if hasattr(plugin, marker): + for plugin_meta in getattr(plugin, marker): + plugin_first(plugin_meta, module_name) + except ImportError: + continue + + +# load modules to work based with and without pyinstaller +# from: https://github.com/webcomics/dosage/blob/master/dosagelib/loader.py +# see: https://github.com/pyinstaller/pyinstaller/issues/1905 +# load modules using iter_modules() +# (should find all plug ins in normal build, but not pyinstaller) +def scan_from_pyinstaller(prefix, path): + # special handling for PyInstaller + table_of_content = set() + for a_toc in (importer.toc for importer in map(pkgutil.get_importer, path) + if hasattr(importer, 'toc')): + table_of_content |= a_toc + + for module_name in table_of_content: + if module_name.startswith(prefix) and '.' not in module_name: + yield module_name diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ffe2fce --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +six diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..482f538 --- /dev/null +++ b/setup.py @@ -0,0 +1,98 @@ +try: + from setuptools import setup, find_packages +except ImportError: + from ez_setup import use_setuptools + use_setuptools() + from setuptools import setup, find_packages +from platform import python_implementation +import sys +PY2 = sys.version_info[0] == 2 +PY26 = PY2 and sys.version_info[1] < 7 + +NAME = 'lml' +AUTHOR = 'C.W.' +VERSION = '0.0.1' +EMAIL = 'wangc_2011(@)hotmail.com' +LICENSE = 'New BSD' +DESCRIPTION = ( + 'Plugin first, Load me later' + + '' +) +KEYWORDS = [ +] + +CLASSIFIERS = [ + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', +] + +INSTALL_REQUIRES = [ + 'six', +] + + +PACKAGES = find_packages(exclude=['ez_setup', 'examples', 'tests']) +EXTRAS_REQUIRE = { +} + + +def read_files(*files): + """Read files into setup""" + text = "" + for single_file in files: + content = read(single_file) + text = text + content + "\n" + return text + + +def read(afile): + """Read a file into setup""" + with open(afile, 'r') as opened_file: + content = filter_out_test_code(opened_file) + content = "".join(list(content)) + return content + + +def filter_out_test_code(file_handle): + found_test_code = False + for line in file_handle.readlines(): + if line.startswith('.. testcode:'): + found_test_code = True + continue + if found_test_code is True: + if line.startswith(' '): + continue + else: + empty_line = line.strip() + if len(empty_line) == 0: + continue + else: + found_test_code = False + yield line + else: + yield line + + +if __name__ == '__main__': + setup( + name=NAME, + author=AUTHOR, + version=VERSION, + author_email=EMAIL, + description=DESCRIPTION, + long_description=read_files('README.rst', 'CHANGELOG.rst'), + license=LICENSE, + keywords=KEYWORDS, + extras_require=EXTRAS_REQUIRE, + tests_require=['nose'], + install_requires=INSTALL_REQUIRES, + packages=PACKAGES, + include_package_data=True, + zip_safe=False, + classifiers=CLASSIFIERS + ) diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..100b35e --- /dev/null +++ b/test.sh @@ -0,0 +1,2 @@ +pip freeze +nosetests --with-cov --cover-package lml --cover-package tests --with-doctest --doctest-extension=.rst README.rst tests docs/source lml && flake8 . --exclude=.moban.d --builtins=unicode,xrange,long diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..b688a93 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,4 @@ +nose +codecov +coverage +flake8 diff --git a/tests/test_plugin_loader.py b/tests/test_plugin_loader.py new file mode 100644 index 0000000..b29f207 --- /dev/null +++ b/tests/test_plugin_loader.py @@ -0,0 +1,82 @@ +from mock import patch +from nose.tools import eq_ + + +@patch('pkgutil.get_importer') +def test_load_from_pyinstaller(pkgutil_get_importer): + sample_toc = set(['pyexcel_io', 'pyexcel_xls', 'blah']) + pkgutil_get_importer.return_value.toc = sample_toc + from lml.plugin import scan_from_pyinstaller + module_names = scan_from_pyinstaller('pyexcel_', 'path') + expected = ['pyexcel_io', 'pyexcel_xls'] + eq_(sorted(list(module_names)), sorted(expected)) + + +@patch('pkgutil.get_importer') +@patch('pkgutil.iter_modules') +@patch('lml.plugin.plugin_first') +def test_load_plugins(pre_register, + pkgutil_iter_modules, + pkgutil_get_importer): + sample_toc = set(['pyexcel_io']) + pkgutil_get_importer.return_value.toc = sample_toc + pkgutil_iter_modules.return_value = [('not used', 'pyexcel_xls', True)] + from lml.plugin import scan_plugins + scan_plugins('pyexcel_', '__pyexcel_io_plugins__', '.', ['pyexcel_io']) + plugin_meta = { + 'plugin_type': 'pyexcel io plugin', + 'file_types': ['xls', 'xlsx', 'xlsm'], + 'submodule': 'xls', + 'stream_type': 'binary' + } + module_name = 'pyexcel_xls' + pre_register.assert_called_with(plugin_meta, module_name) + + +@patch('pkgutil.get_importer') +@patch('pkgutil.iter_modules') +@patch('lml.plugin.plugin_first') +def test_load_plugins_without_pyinstaller(pre_register, + pkgutil_iter_modules, + pkgutil_get_importer): + sample_toc = set() + pkgutil_get_importer.return_value.toc = sample_toc + pkgutil_iter_modules.return_value = [('not used', 'pyexcel_xls', True)] + from lml.plugin import scan_plugins + scan_plugins('pyexcel_', '__pyexcel_io_plugins__', '.', ['pyexcel_io']) + plugin_meta = { + 'plugin_type': 'pyexcel io plugin', + 'file_types': ['xls', 'xlsx', 'xlsm'], + 'submodule': 'xls', + 'stream_type': 'binary' + } + module_name = 'pyexcel_xls' + pre_register.assert_called_with(plugin_meta, module_name) + + +@patch('pkgutil.get_importer') +@patch('pkgutil.iter_modules') +@patch('lml.plugin.plugin_first') +def test_load_plugins_without_any_plugins(pre_register, + pkgutil_iter_modules, + pkgutil_get_importer): + sample_toc = set() + pkgutil_get_importer.return_value.toc = sample_toc + pkgutil_iter_modules.return_value = [] + from lml.plugin import scan_plugins + scan_plugins('pyexcel_', '__pyexcel_io_plugins__', '.', ['pyexcel_io']) + assert pre_register.called is False + + +@patch('pkgutil.get_importer') +@patch('pkgutil.iter_modules') +@patch('lml.plugin.plugin_first') +def test_load_plugins_import_error(pre_register, + pkgutil_iter_modules, + pkgutil_get_importer): + sample_toc = set(['test_non_existent_module']) + pkgutil_get_importer.return_value.toc = sample_toc + pkgutil_iter_modules.return_value = [('not used', 'pyexcel_xls', False)] + from lml.plugin import scan_plugins + scan_plugins('test_', '__pyexcel_io_plugins__', '.', ['pyexcel_io']) + assert pre_register.called is False