diff --git a/.github/workflows/moban-update.yml b/.github/workflows/moban-update.yml new file mode 100644 index 0000000..706fd82 --- /dev/null +++ b/.github/workflows/moban-update.yml @@ -0,0 +1,29 @@ +on: [push] + +jobs: + run_moban: + runs-on: ubuntu-latest + name: synchronize templates via moban + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: '3.7' + - name: check changes + run: | + pip install moban gitfs2 pypifs moban-jinja2-github moban-ansible + moban + git status + git diff --exit-code + - name: Auto-commit + if: failure() + uses: docker://cdssnc/auto-commit-github-action + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: >- + This is an auto-commit, updating project meta data, + such as changelog.rst, contributors.rst diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml new file mode 100644 index 0000000..9e7ec42 --- /dev/null +++ b/.github/workflows/pythonpublish.yml @@ -0,0 +1,26 @@ +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* 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/.gitmodules b/.gitmodules new file mode 100644 index 0000000..80100f8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,27 @@ +[submodule "examples/robotchef_allinone"] + path = examples/robotchef_allinone + url = https://github.com/python-lml/robotchef_allinone.git +[submodule "examples/robotchef_allinone_lml"] + path = examples/robotchef_allinone_lml + url = https://github.com/python-lml/robotchef_allinone_lml.git +[submodule "examples/robotchef"] + path = examples/robotchef + url = https://github.com/python-lml/robotchef.git +[submodule "examples/robotchef_cook"] + path = examples/robotchef_cook + url = https://github.com/python-lml/robotchef_cook.git +[submodule "examples/robotchef_britishcuisine"] + path = examples/robotchef_britishcuisine + url = https://github.com/python-lml/robotchef_britishcuisine.git +[submodule "examples/robotchef_chinesecuisine"] + path = examples/robotchef_chinesecuisine + url = https://github.com/python-lml/robotchef_chinesecuisine.git +[submodule "examples/v2/robotchef_v2"] + path = examples/v2/robotchef_v2 + url = https://github.com/python-lml/robotchef_v2.git +[submodule "examples/v2/robotchef_britishcuisine"] + path = examples/v2/robotchef_britishcuisine + url = https://github.com/python-lml/robotchef_britishcuisine_v2 +[submodule "examples/v2/robotchef_api"] + path = examples/v2/robotchef_api + url = https://github.com/python-lml/robotchef_api diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..646d29a --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,10 @@ +[settings] +line_length=79 +known_first_party= +known_third_party=mock,nose +indent=' ' +multi_line_output=3 +length_sort=1 +default_section=FIRSTPARTY +no_lines_before=LOCALFOLDER +sections=FUTURE,STDLIB,FIRSTPARTY,THIRDPARTY,LOCALFOLDER diff --git a/.moban.d/CUSTOM_README.rst.jj2 b/.moban.d/CUSTOM_README.rst.jj2 new file mode 100644 index 0000000..12b7c34 --- /dev/null +++ b/.moban.d/CUSTOM_README.rst.jj2 @@ -0,0 +1,18 @@ +{% extends "README.rst.jj2" %} + +{%block features%} +{%include "description.rst.jj2" %} +{%endblock%} + +{%block bottom_block%} +lml enabled project +================================================================================ + +{%include "lml-enabled-projects.rst.jj2" %} + +License +================================================================================ + +New BSD + +{%endblock%} \ No newline at end of file diff --git a/.moban.d/custom_setup.py.jj2 b/.moban.d/custom_setup.py.jj2 new file mode 100644 index 0000000..ac3890c --- /dev/null +++ b/.moban.d/custom_setup.py.jj2 @@ -0,0 +1,5 @@ +{%extends "setup.py.jj2"%} + + +{%block platform_block%} +{%endblock %} \ No newline at end of file diff --git a/.moban.d/description.rst.jj2 b/.moban.d/description.rst.jj2 new file mode 100644 index 0000000..046fe56 --- /dev/null +++ b/.moban.d/description.rst.jj2 @@ -0,0 +1,55 @@ +**{{name}}** seamlessly finds the lml based plugins from your current python +environment but loads your plugins on demand. It is designed to support +plugins that have external dependencies, especially bulky and/or +memory hungry ones. {{name}} provides the plugin management system only and the +plugin interface is on your shoulder. + +**{{name}}** enabled applications helps your customers [#f1]_ in two ways: + +#. Your customers could cherry-pick the plugins from pypi per python environment. + They could remove a plugin using `pip uninstall` command. +#. Only the plugins used at runtime gets loaded into computer memory. + +When you would use **lml** to refactor your existing code, it aims to flatten the +complexity and to shrink the size of your bulky python library by +distributing the similar functionalities across its plugins. However, you as +the developer need to do the code refactoring by yourself and lml would lend you a hand. + +.. [#f1] the end developers who uses your library and packages achieve their + objectives. + + +Quick start +================================================================================ + +The following code tries to get you started quickly with **non-lazy** loading. + +.. code-block:: python + + from lml.plugin import PluginInfo, PluginManager + + + @PluginInfo("cuisine", tags=["Portable Battery"]) + class Boost(object): + def make(self, food=None, **keywords): + print("I can cook %s for robots" % food) + + + class CuisineManager(PluginManager): + def __init__(self): + PluginManager.__init__(self, "cuisine") + + def get_a_plugin(self, food_name=None, **keywords): + return PluginManager.get_a_plugin(self, key=food_name, **keywords) + + + if __name__ == '__main__': + manager = CuisineManager() + chef = manager.get_a_plugin("Portable Battery") + chef.make() + + +At a glance, above code simply replaces the Factory pattern should you write +them without lml. What's not obvious is, that once you got hands-on with it, +you can start work on how to do **lazy** loading. + diff --git a/.moban.d/docs/source/custom_conf.py.jj2 b/.moban.d/docs/source/custom_conf.py.jj2 new file mode 100644 index 0000000..e2378f1 --- /dev/null +++ b/.moban.d/docs/source/custom_conf.py.jj2 @@ -0,0 +1,3 @@ +{% include "docs/source/myconf.py.jj2" %} + +master_doc = "index" \ No newline at end of file diff --git a/.moban.d/docs/source/custom_index.rst.jj2 b/.moban.d/docs/source/custom_index.rst.jj2 new file mode 100644 index 0000000..5094d13 --- /dev/null +++ b/.moban.d/docs/source/custom_index.rst.jj2 @@ -0,0 +1,30 @@ +`{{name}}` - {{description}} +================================================================================ + + +:Author: C.W. +:Source code: http://github.com/{{name}}/{{name}}.git +:Issues: http://github.com/{{name}}/{{name}}/issues +:License: New BSD License +:Released: |version| +:Generated: |today| + + +Introduction +------------- + +{% include "description.rst.jj2" %} + + +Documentation +---------------- + +.. toctree:: + :maxdepth: 2 + + design + tutorial + lml_log + api + +{%include "lml-enabled-projects.rst.jj2" %} diff --git a/.moban.d/docs/source/myconf.py.jj2 b/.moban.d/docs/source/myconf.py.jj2 new file mode 100644 index 0000000..348424d --- /dev/null +++ b/.moban.d/docs/source/myconf.py.jj2 @@ -0,0 +1,11 @@ +{%extends "docs/source/conf.py.jj2"%} + +{%block SPHINX_EXTENSIONS%} + 'sphinx.ext.napoleon', + 'sphinxcontrib.spelling' +{%endblock%} + +{%block additional_config%} +spelling_lang = 'en_GB' +spelling_word_list_filename = 'spelling_wordlist.txt' +{%endblock%} \ No newline at end of file diff --git a/.moban.d/lml-enabled-projects.rst.jj2 b/.moban.d/lml-enabled-projects.rst.jj2 new file mode 100644 index 0000000..84ffc86 --- /dev/null +++ b/.moban.d/lml-enabled-projects.rst.jj2 @@ -0,0 +1,12 @@ +Beyond the documentation above, here is a list of projects using lml: + +#. `pyexcel `_ +#. `pyecharts `_ +#. `moban `_ + +lml is available on these distributions: + +#. `ARCH linux `_ +#. `Conda forge `_ +#. `OpenSuse `_ + 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/test.sh.jj2 b/.moban.d/test.sh.jj2 new file mode 100644 index 0000000..b47acda --- /dev/null +++ b/.moban.d/test.sh.jj2 @@ -0,0 +1,19 @@ +{%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 +cd tests/test_plugin +python setup.py install +cd - +pytest tests --verbosity=3 --cov=lml --doctest-glob=*.rst && flake8 . --exclude=.moban.d,docs,setup.py {%block flake8_options%}--builtins=unicode,xrange,long{%endblock%} + +{%block posttest%} +{%endblock%} diff --git a/.moban.yml b/.moban.yml new file mode 100644 index 0000000..6c48a69 --- /dev/null +++ b/.moban.yml @@ -0,0 +1,21 @@ +configuration: + template_dir: + - "git://github.com/moremoban/pypi-mobans.git?submodule=true&brach=dev!/statics" + - "git://github.com/moremoban/pypi-mobans.git?submodule=true&branch=dev!/templates" + - ".moban.d" + configuration: lml.yml +targets: + - README.rst: CUSTOM_README.rst.jj2 + - setup.py: custom_setup.py.jj2 + - requirements.txt: requirements.txt.jj2 + - "docs/source/conf.py": "docs/source/custom_conf.py.jj2" + - "docs/source/index.rst": "docs/source/custom_index.rst.jj2" + - test.sh: test.sh.jj2 + - "lml/_version.py": _version.py.jj2 + - output: CHANGELOG.rst + configuration: changelog.yml + template: CHANGELOG.rst.jj2 + - ".github/workflows/pythonpublish.yml": "pythonpublish.yml" + - ".github/workflows/moban-update.yml": "moban-update.yml" + - CONTRIBUTORS.rst: CONTRIBUTORS.rst.jj2 + - MANIFEST.in: MANIFEST.in.jj2 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..21426f2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +sudo: false +language: python +notifications: + email: false +python: + - 3.9-dev + - 3.8 + - 3.7 + - 3.6 +before_install: + - pip install -r tests/requirements.txt +script: + - make test +after_success: + codecov diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5f0bf77..f6f22a6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,65 +1,67 @@ Change log ================================================================================ + +0.1.0 - 21/10/2020 +-------------------------------------------------------------------------------- + +**Updated** + +#. non class object can be a plugin too +#. `#20 `_: When a plugin was not + installed, it now calls raise_exception method 0.0.9 - 7/1/2019 -------------------------------------------------------------------------------- -Updated -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +**Updated** -#. `#11 `_: more test contents for +#. `#11 `_: more test contents for OpenSuse package validation 0.0.8 - 4/1/2019 -------------------------------------------------------------------------------- -Updated -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +**Updated** -#. `#9 `_: include tests, docs for +#. `#9 `_: include tests, docs for OpenSuse package validation 0.0.7 - 17/11/2018 -------------------------------------------------------------------------------- -Fixed -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +**Fixed** -#. `#8 `_: get_primary_key will fail when - a module is loaded later +#. `#8 `_: get_primary_key will fail + when a module is loaded later #. deprecated old style plugin scanner: scan_plugins 0.0.6 - 07/11/2018 -------------------------------------------------------------------------------- -Fixed -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +**Fixed** #. Revert the version 0.0.5 changes. Raise Import error and log the exception 0.0.5 - 06/11/2018 -------------------------------------------------------------------------------- -Fixed -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +**Fixed** -#. `#6 `_: Catch and Ignore +#. `#6 `_: Catch and Ignore ModuleNotFoundError 0.0.4 - 07.08.2018 -------------------------------------------------------------------------------- -Added -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +**Added** -#. `#4 `_: to find plugin names with +#. `#4 `_: to find plugin names with different naming patterns 0.0.3 - 12/06/2018 -------------------------------------------------------------------------------- -Added -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +**Added** #. `dict` can be a pluggable type in addition to `function`, `class` #. get primary tag of your tag, helping you find out which category of plugins @@ -68,8 +70,7 @@ 0.0.2 - 23/10/2017 -------------------------------------------------------------------------------- -Updated -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +**Updated** #. `pyexcel#103 `_: include LICENSE in tar ball @@ -77,7 +78,6 @@ 0.0.1 - 30/05/2017 -------------------------------------------------------------------------------- -Added -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +**Added** #. First release diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst new file mode 100644 index 0000000..4165d31 --- /dev/null +++ b/CONTRIBUTORS.rst @@ -0,0 +1,9 @@ + + +2 contributors +================================================================================ + +In alphabetical order: + +* `Ayan Banerjee `_ +* `Matěj Cepl `_ diff --git a/MANIFEST.in b/MANIFEST.in index c5f22c6..e86ae54 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include README.rst include LICENSE include CHANGELOG.rst +include CONTRIBUTORS.rst recursive-include tests * -include docs/source/* +recursive-include docs * diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f993b37 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +all: test + +test: + bash test.sh + +document: + python setup.py install + sphinx-build -b html docs/source/ docs/build + +spelling: + sphinx-build -b spelling docs/source/ docs/build/spelling + +uml: + plantuml -tsvg -o ../_static/images/ docs/source/uml/*.uml + +format: + isort -y $(find lml -name "*.py"|xargs echo) $(find tests -name "*.py"|xargs echo) + black -l 79 lml + black -l 79 tests + black -l 79 examples diff --git a/PKG-INFO b/PKG-INFO deleted file mode 100644 index 1d394bb..0000000 --- a/PKG-INFO +++ /dev/null @@ -1,163 +0,0 @@ -Metadata-Version: 1.1 -Name: lml -Version: 0.0.9 -Summary: Load me later. A lazy plugin management system. -Home-page: https://github.com/chfw/lml -Author: C.W. -Author-email: wangc_2011@hotmail.com -License: New BSD -Download-URL: https://github.com/chfw/lml/archive/0.0.9.tar.gz -Description: ================================================================================ - lml - Load me later. A lazy plugin management system. - ================================================================================ - - .. image:: https://api.travis-ci.org/chfw/lml.svg - :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/ - - **lml** seamlessly finds the lml based plugins from your current python - environment but loads your plugins on demand. It is designed to support - plugins that have external dependencies, especially bulky and/or - memory hungry ones. lml provides the plugin management system only and the - plugin interface is on your shoulder. - - **lml** enabled applications helps your customers [#f1]_ in two ways: - - #. Your customers could cherry-pick the plugins from pypi per python environment. - They could remove a plugin using `pip uninstall` command. - #. Only the plugins used at runtime gets loaded into computer memory. - - When you would use **lml** to refactor your existing code, it aims to flatten the - complexity and to shrink the size of your bulky python library by - distributing the similar functionalities across its plugins. However, you as - the developer need to do the code refactoring by yourself and lml would lend you a hand. - - .. [#f1] the end developers who uses your library and packages achieve their - objectives. - - Installation - ================================================================================ - - - You can install lml via pip: - - .. code-block:: bash - - $ pip install lml - - - or clone it and install it: - - .. code-block:: bash - - $ git clone https://github.com/chfw/lml.git - $ cd lml - $ python setup.py install - - License - ================================================================================ - - New BSD - - Change log - ================================================================================ - - 0.0.9 - 7/1/2019 - -------------------------------------------------------------------------------- - - Updated - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. `#11 `_: more test contents for - OpenSuse package validation - - 0.0.8 - 4/1/2019 - -------------------------------------------------------------------------------- - - Updated - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. `#9 `_: include tests, docs for - OpenSuse package validation - - 0.0.7 - 17/11/2018 - -------------------------------------------------------------------------------- - - Fixed - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. `#8 `_: get_primary_key will fail when - a module is loaded later - #. deprecated old style plugin scanner: scan_plugins - - 0.0.6 - 07/11/2018 - -------------------------------------------------------------------------------- - - Fixed - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. Revert the version 0.0.5 changes. Raise Import error and log the exception - - 0.0.5 - 06/11/2018 - -------------------------------------------------------------------------------- - - Fixed - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. `#6 `_: Catch and Ignore - ModuleNotFoundError - - 0.0.4 - 07.08.2018 - -------------------------------------------------------------------------------- - - Added - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. `#4 `_: to find plugin names with - different naming patterns - - 0.0.3 - 12/06/2018 - -------------------------------------------------------------------------------- - - Added - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. `dict` can be a pluggable type in addition to `function`, `class` - #. get primary tag of your tag, helping you find out which category of plugins - your tag points to - - 0.0.2 - 23/10/2017 - -------------------------------------------------------------------------------- - - Updated - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. `pyexcel#103 `_: include - LICENSE in tar ball - - 0.0.1 - 30/05/2017 - -------------------------------------------------------------------------------- - - Added - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. First release - - -Keywords: python -Platform: UNKNOWN -Classifier: Topic :: Software Development :: Libraries -Classifier: Programming Language :: Python -Classifier: Intended Audience :: Developers -Classifier: Programming Language :: Python :: 2.6 -Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3.3 -Classifier: Programming Language :: Python :: 3.4 -Classifier: Programming Language :: Python :: 3.5 -Classifier: Programming Language :: Python :: 3.6 diff --git a/README.rst b/README.rst index 665349a..ecc9100 100644 --- a/README.rst +++ b/README.rst @@ -2,12 +2,25 @@ lml - Load me later. A lazy plugin management system. ================================================================================ -.. image:: https://api.travis-ci.org/chfw/lml.svg - :target: http://travis-ci.org/chfw/lml +.. image:: https://api.travis-ci.org/python-lml/lml.svg + :target: http://travis-ci.org/python-lml/lml -.. image:: https://codecov.io/github/chfw/lml/coverage.png - :target: https://codecov.io/github/chfw/lml +.. image:: https://codecov.io/github/python-lml/lml/coverage.png + :target: https://codecov.io/github/python-lml/lml +.. image:: https://badge.fury.io/py/lml.svg + :target: https://pypi.org/project/lml +.. image:: https://pepy.tech/badge/lml/month + :target: https://pepy.tech/project/lml/month + +.. image:: https://img.shields.io/github/stars/python-lml/lml.svg?style=social&maxAge=3600&label=Star + :target: https://github.com/python-lml/lml/stargazers + +.. image:: https://img.shields.io/static/v1?label=continuous%20templating&message=%E6%A8%A1%E7%89%88%E6%9B%B4%E6%96%B0&color=blue&style=flat-square + :target: https://moban.readthedocs.io/en/latest/#at-scale-continous-templating-for-open-source-projects + +.. image:: https://img.shields.io/static/v1?label=coding%20style&message=black&color=black&style=flat-square + :target: https://github.com/psf/black .. image:: https://readthedocs.org/projects/lml/badge/?version=latest :target: http://lml.readthedocs.org/en/latest/ @@ -32,6 +45,42 @@ .. [#f1] the end developers who uses your library and packages achieve their objectives. + +Quick start +================================================================================ + +The following code tries to get you started quickly with **non-lazy** loading. + +.. code-block:: python + + from lml.plugin import PluginInfo, PluginManager + + + @PluginInfo("cuisine", tags=["Portable Battery"]) + class Boost(object): + def make(self, food=None, **keywords): + print("I can cook %s for robots" % food) + + + class CuisineManager(PluginManager): + def __init__(self): + PluginManager.__init__(self, "cuisine") + + def get_a_plugin(self, food_name=None, **keywords): + return PluginManager.get_a_plugin(self, key=food_name, **keywords) + + + if __name__ == '__main__': + manager = CuisineManager() + chef = manager.get_a_plugin("Portable Battery") + chef.make() + + +At a glance, above code simply replaces the Factory pattern should you write +them without lml. What's not obvious is, that once you got hands-on with it, +you can start work on how to do **lazy** loading. + + Installation ================================================================================ @@ -47,9 +96,25 @@ .. code-block:: bash - $ git clone https://github.com/chfw/lml.git + $ git clone https://github.com/python-lml/lml.git $ cd lml $ python setup.py install + +lml enabled project +================================================================================ + +Beyond the documentation above, here is a list of projects using lml: + +#. `pyexcel `_ +#. `pyecharts `_ +#. `moban `_ + +lml is available on these distributions: + +#. `ARCH linux `_ +#. `Conda forge `_ +#. `OpenSuse `_ + License ================================================================================ diff --git a/changelog.yml b/changelog.yml new file mode 100644 index 0000000..f578917 --- /dev/null +++ b/changelog.yml @@ -0,0 +1,66 @@ +name: lml +organisation: python-lml +releases: +- changes: + - action: Updated + details: + - "non class object can be a plugin too" + - "`#20`: When a plugin was not installed, it now calls raise_exception method" + date: 21/10/2020 + version: 0.1.0 +- changes: + - action: Updated + details: + - "`#11`: more test contents for OpenSuse package validation" + date: 7/1/2019 + version: 0.0.9 +- changes: + - action: Updated + details: + - "`#9`: include tests, docs for OpenSuse package validation" + date: 4/1/2019 + version: 0.0.8 +- changes: + - action: Fixed + details: + - "`#8`: get_primary_key will fail when a module is loaded later" + - "deprecated old style plugin scanner: scan_plugins" + date: 17/11/2018 + version: 0.0.7 +- changes: + - action: Fixed + details: + - "Revert the version 0.0.5 changes. Raise Import error and log the exception" + date: 07/11/2018 + version: 0.0.6 +- changes: + - action: Fixed + details: + - "`#6`: Catch and Ignore ModuleNotFoundError" + date: 06/11/2018 + version: 0.0.5 +- changes: + - action: Added + details: + - "`#4`: to find plugin names with different naming patterns" + date: 07.08.2018 + version: 0.0.4 +- changes: + - action: Added + details: + - "`dict` can be a pluggable type in addition to `function`, `class`" + - get primary tag of your tag, helping you find out which category of plugins your tag points to + date: 12/06/2018 + version: 0.0.3 +- changes: + - action: Updated + details: + - "`pyexcel#103 `_: include LICENSE in tar ball" + date: 23/10/2017 + version: 0.0.2 +- changes: + - action: Added + details: + - First release + date: 30/05/2017 + version: 0.0.1 diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..418c66f --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinxcontrib-plantuml + diff --git a/docs/source/_static/images/loading_sequence.svg b/docs/source/_static/images/loading_sequence.svg new file mode 100644 index 0000000..732fad4 --- /dev/null +++ b/docs/source/_static/images/loading_sequence.svg @@ -0,0 +1,32 @@ +bobbobrobotchefrobotcheflmllmlrobotchef_britishcuisinerobotchef_britishcuisine> robotchef "Jacket Potato"scan for plugins.read plugin chain in the moduleI can help with "Jacket Potato" and others.read the built-in robot_cuisinebuilt-in chef knows "Portable Battery"scanning doneget me a plugin that knows "Jacket Potato"robotchef_britishcuisine.bake.Bake can domake the food"I can bake Jacket Potato" \ No newline at end of file diff --git a/docs/source/_static/images/robot_chef.svg b/docs/source/_static/images/robot_chef.svg new file mode 100644 index 0000000..516e767 --- /dev/null +++ b/docs/source/_static/images/robot_chef.svg @@ -0,0 +1,22 @@ +ChefBoostFryBake \ No newline at end of file diff --git a/docs/source/_static/images/robotchef_allinone_lml.svg b/docs/source/_static/images/robotchef_allinone_lml.svg new file mode 100644 index 0000000..61cebb2 --- /dev/null +++ b/docs/source/_static/images/robotchef_allinone_lml.svg @@ -0,0 +1,38 @@ +lmlrobotchef_allinone_lmlPluginManagerPluginInfoCuisineManagerget_a_plugin()raise_exception()Chefmake()BoostFryBakecuisine \ No newline at end of file diff --git a/docs/source/_static/images/robotchef_api_crd.svg b/docs/source/_static/images/robotchef_api_crd.svg new file mode 100644 index 0000000..ca86aa5 --- /dev/null +++ b/docs/source/_static/images/robotchef_api_crd.svg @@ -0,0 +1,50 @@ +lmlrobotchef_apirobotchef.robot_cuisinerobotchef_britishcuisinePluginManagerPluginInfoChainPluginInfoCuisineManagerget_a_plugin()raise_exception()Chefmake()BoostFryBakerobotchef_v2registers plugin infocuisine \ No newline at end of file diff --git a/docs/source/_static/images/robotchef_crd.svg b/docs/source/_static/images/robotchef_crd.svg new file mode 100644 index 0000000..f6b1b3a --- /dev/null +++ b/docs/source/_static/images/robotchef_crd.svg @@ -0,0 +1,45 @@ +lmlrobotchefrobotchef.robot_cuisinerobotchef_britishcuisinePluginManagerPluginInfoChainPluginInfoCuisineManagerget_a_plugin()raise_exception()Chefmake()BoostFryBakeregisters plugin infocuisine \ No newline at end of file diff --git a/docs/source/allinone_lml_tutorial.rst b/docs/source/allinone_lml_tutorial.rst index 6165854..ddbb3ed 100644 --- a/docs/source/allinone_lml_tutorial.rst +++ b/docs/source/allinone_lml_tutorial.rst @@ -12,10 +12,9 @@ Demo -------------------------------------------------------------------------------- -Please navigate to -`lml/examples `_, -you would find robotchef_allinone_lml and its packages. Do the following:: +Please navigate to robotchef_allinone_lml and its packages. Do the following:: + $ git clone https://github.com/python-lml/robotchef_allinone_lml $ cd robotchef_allinone_lml $ python setup.py install diff --git a/docs/source/allinone_tutorial.rst b/docs/source/allinone_tutorial.rst index 0606d7d..0db77a9 100644 --- a/docs/source/allinone_tutorial.rst +++ b/docs/source/allinone_tutorial.rst @@ -7,13 +7,9 @@ Demo -------------------------------------------------------------------------------- -Please checkout lml:: +Please checkout the robot chef example:: - $ git clone https://github.com/chfw/lml.git - -And navigate to `lml/examples `_, -you would find robotchef_allinone and its packages. Do the following:: - + $ git clone https://github.com/python-lml/robotchef_allinone $ cd robotchef_allinone $ python setup.py install @@ -40,7 +36,7 @@ .. literalinclude:: ../../examples/robotchef_allinone/robotchef_allinone/plugin.py :language: python - :lines: 5-26 + :lines: 5-30 Line 13, class `Chef` defines the plugin class interface. For robotchef, `make` is defined to illustrate the functionality. Naturally you will be deciding the @@ -61,7 +57,7 @@ .. literalinclude:: ../../examples/robotchef_allinone/robotchef_allinone/plugin.py :language: python - :lines: 29- + :lines: 33- main.py +++++++++++ diff --git a/docs/source/api.rst b/docs/source/api.rst index 98ad4ac..7159447 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -3,8 +3,8 @@ .. automodule:: lml.loader -.. autofunction:: scan_plugins - +.. autofunction:: scan_plugins_regex + .. automodule:: lml.plugin .. autoclass:: PluginInfo @@ -13,4 +13,3 @@ .. autoclass:: PluginManager :members: - diff --git a/docs/source/api_tutorial.rst b/docs/source/api_tutorial.rst index e33b341..bb49c2d 100644 --- a/docs/source/api_tutorial.rst +++ b/docs/source/api_tutorial.rst @@ -11,13 +11,15 @@ Demo -------------------------------------------------------------------------------- -Navigate to `lml/examples/v2 `_, -you would find robotchef and its packages. Do the following:: +Please checkout the following examples:: $ virtualenv --no-site-packages robotchefv2 $ source robotchefv2/bin/activate + $ git clone https://github.com/python-lml/robotchef_v2 $ cd robotchef_v2 $ python setup.py install + $ cd .. + $ git clone https://github.com/python-lml/robotchef_api $ cd robotchef_api $ python setup.py install @@ -31,7 +33,8 @@ In order to add "Jacket Potato" in the know-how, you would need to install robotchef_britishcuisine in this folder:: - $ cd robotchef_britishcuisine + $ git clone https://github.com/python-lml/robotchef_britishcuisine_v2 + $ cd robotchef_britishcuisine_v2 $ python setup.py install $ robotchef_v2 "Jacket Potato" I can bake Jacket Potato @@ -65,7 +68,7 @@ .. literalinclude:: ../../examples/v2/robotchef_api/robotchef_api/__init__.py :language: python -scan_plugins here loads all modules that start with "robotchef_" and as well as +scan_plugins_regex here loads all modules that start with "robotchef_" and as well as the module `robotchef_api.robot_cuisine` in the white_list. This is how you will write the main component as a library. diff --git a/docs/source/conf.py b/docs/source/conf.py index a6c609c..ed08dc1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -3,13 +3,11 @@ 'Load me later. A lazy plugin management system.' + '' ) -# -*- coding: utf-8 -*- -# # Configuration file for the Sphinx documentation builder. # -# This file does only contain a selection of the most common options. For a -# full list see the documentation: -# http://www.sphinx-doc.org/en/master/config +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- @@ -23,21 +21,15 @@ # -- Project information ----------------------------------------------------- -project = u'lml' -copyright = u'2017-2019 Onni Software Ltd.' -author = u'C.W.' - +project = 'lml' +copyright = '2017-2020 Onni Software Ltd.' +author = 'C.W.' # The short X.Y version -version = u'0.0.9' +version = '0.1.0' # The full version, including alpha/beta/rc tags -release = u'0.0.9' - +release = '0.1.0' # -- General configuration --------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -46,15 +38,6 @@ # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -68,9 +51,6 @@ # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = None - # -- Options for HTML output ------------------------------------------------- @@ -79,107 +59,16 @@ # html_theme = 'alabaster' -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# The default sidebars (for documents that don't match any pattern) are -# defined by theme itself. Builtin themes are using these templates by -# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', -# 'searchbox.html']``. -# -# html_sidebars = {} - - -# -- Options for HTMLHelp output --------------------------------------------- - -# Output file base name for HTML help builder. -htmlhelp_basename = 'lmldoc' - - -# -- Options for LaTeX output ------------------------------------------------ - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'lml.tex', u'lml Documentation', - u'Onni Software Ltd.', 'manual'), -] - - -# -- Options for manual page output ------------------------------------------ - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'lml', u'lml Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ---------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'lml', u'lml Documentation', - author, 'lml', 'One line description of project.', - 'Miscellaneous'), -] - - -# -- Options for Epub output ------------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = project - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -# -# epub_identifier = '' - -# A unique identification for the text. -# -# epub_uid = '' - -# A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] - # -- Extension configuration ------------------------------------------------- # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} +intersphinx_mapping = {'https://docs.python.org/3/': None} # TODO: html_theme not configurable upstream html_theme = 'default' @@ -193,3 +82,6 @@ ] intersphinx_mapping.update({ }) +master_doc = "index" + +master_doc = "index" \ No newline at end of file diff --git a/docs/source/design.rst b/docs/source/design.rst index b3710b2..766575e 100644 --- a/docs/source/design.rst +++ b/docs/source/design.rst @@ -6,10 +6,20 @@ its code growth, the code in pyexcel packages to manage the external and internal plugins becomes a independent library, lml. -lml is similar to **Factories** in -Zope Component Architecture [#f2]_. Lml provides functionalities to +Lml is similar to **Factories** in +Zope Component Architecture [#f2]_. It provides functionalities to discover, register and load lml based plugins. It cares how the meta data were -written but it does care how the plugin interface is written. +written but it does NOT care how the plugin interface is written. + +Simply, lml promises to load your external dependency when they are used, but +only when you follow lazy-loading design principle below. Otherwise, lml does +immediate import and takes away the developer's responsibility to manage plugin +registry and discovery. + +In terms of extensibility of your proud package, lml keeps the door open even +if you use lml for immediate import. As a developer, you give the choice to other +contributor to write up a plugin for your package. As long as the user would have +installed community created extensions, lml will discover them and use them. Plugin discovery @@ -20,7 +30,7 @@ It allows the developer to split a bigger packages into a smaller ones and publish them separately. sphinxcontrib [#f4]_ uses a typical namespace package based method. However, namespace package places a strict requirement -on the module's __init__.py: nothing other than name space declaration should +on the module's `__init__.py`: nothing other than name space declaration should be present. It means no module level functions can be place there. This restriction forces the plugin to be driven by the main package but the plugin cannot use the main package as its own library to do specific things. So namespace package @@ -40,7 +50,7 @@ development since 2016. In order to overcome those shortcomings, implicit imports were coded into module's -__init__.py. By iterating through currently installed modules in your python +`__init__.py`. By iterating through currently installed modules in your python environment, the relevant plugins are imported automatically. lml uses implicit import. In order to manage the plugins, pip can be used to @@ -49,29 +59,27 @@ python path, you can nominate one plugin to be picked. Plugin registration ---------------------- +-------------------------------------------------------------------------------- In terms of plugin registrations, three different approaches have been tried. Monkey-patching was easy to implement. When a plugin is imported, it loads -the plugin dictionary from the main package and add itself. -But it is generally perceived as a "bad" idea. -Another way of doing it is to place -the plugin code in the main component and the plugin just need to declare a -dictionary as the plugin's meta data. The main package register the meta data -when it is imported. tablib [#f5]_ uses such a approach. -The third way is to use meta-classes. M. Alchin (2008) [#f6]_ explained how meta class can -be used to register plugin classes in a simpler way. +the plugin dictionary from the main package and add itself. But it is generally +perceived as a "bad" idea. Another way of doing it is to place the plugin code +in the main component and the plugin just need to declare a dictionary as the +plugin's meta data. The main package register the meta data when it is imported. +tablib [#f5]_ uses such a approach. The third way is to use meta-classes. +M. Alchin (2008) [#f6]_ explained how meta class can be used to register plugin +classes in a simpler way. lml uses meta data for plugin registration. Since lml load your plugin later, -the meta data is stored in the module's __init__.py. For example, to load plugins later -in tablib, the 'exports' variable should be taken out from the actual class file and -replace the hard reference to the classes with class path string. +the meta data is stored in the module's __init__.py. For example, to load plugins +later in tablib, the 'exports' variable should be taken out from the actual +class file and replace the hard reference to the classes with class path string. Plugin distribution --------------------- -In terms of plugin distribution, yapsy [#f7]_ and GEdit plugin management -system [#f8]_ load plugins from file system. +yapsy [#f7]_ and GEdit plugin management system [#f8]_ load plugins from file system. To install a plugin in those systems, is to copy and paste the plugin code to a designated directory. zope components, namespace packages and flask extensions can be installed via pypi. lml support the latter approach. lml plugins can be @@ -81,7 +89,7 @@ ------------------ To use lml, it asks you to avoid importing your "heavy" dependencies -in __init__.py. lml also respects the independence of individual packages. You can +in `__init__.py`. lml respects the independence of individual packages. You can put modular level functions in your __init__.py as long as it does not trigger immediate import of your dependency. This is to allow the individual plugin to become useful as it is, rather to be integrated with your main package. For example, @@ -90,7 +98,6 @@ With lml, as long as your third party developer respect the plugin name prefix, they could publish their plugins as they do to any normal pypi packages. And the end developer of yours would only need to do pip install. - References ------------- diff --git a/docs/source/index.rst b/docs/source/index.rst index 5c577f5..5254787 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -34,6 +34,42 @@ objectives. +Quick start +================================================================================ + +The following code tries to get you started quickly with **non-lazy** loading. + +.. code-block:: python + + from lml.plugin import PluginInfo, PluginManager + + + @PluginInfo("cuisine", tags=["Portable Battery"]) + class Boost(object): + def make(self, food=None, **keywords): + print("I can cook %s for robots" % food) + + + class CuisineManager(PluginManager): + def __init__(self): + PluginManager.__init__(self, "cuisine") + + def get_a_plugin(self, food_name=None, **keywords): + return PluginManager.get_a_plugin(self, key=food_name, **keywords) + + + if __name__ == '__main__': + manager = CuisineManager() + chef = manager.get_a_plugin("Portable Battery") + chef.make() + + +At a glance, above code simply replaces the Factory pattern should you write +them without lml. What's not obvious is, that once you got hands-on with it, +you can start work on how to do **lazy** loading. + + + Documentation ---------------- @@ -47,6 +83,12 @@ Beyond the documentation above, here is a list of projects using lml: -#. `pyexcel `_ -#. `pyexcel-io `_ -#. `pyexcel-chart `_ +#. `pyexcel `_ +#. `pyecharts `_ +#. `moban `_ + +lml is available on these distributions: + +#. `ARCH linux `_ +#. `Conda forge `_ +#. `OpenSuse `_ diff --git a/docs/source/lml_log.rst b/docs/source/lml_log.rst index 996a7e5..c22e54f 100644 --- a/docs/source/lml_log.rst +++ b/docs/source/lml_log.rst @@ -8,7 +8,7 @@ Enable the logging ------------------- -Let us open robotchef's `main.py `_. Insert the highlighted codes. +Let us open robotchef's `main.py `_. Insert the highlighted codes. .. code-block:: python :emphasize-lines: 5-10 diff --git a/docs/source/lml_tutorial.rst b/docs/source/lml_tutorial.rst index ad858a5..a6d8d7f 100644 --- a/docs/source/lml_tutorial.rst +++ b/docs/source/lml_tutorial.rst @@ -9,10 +9,9 @@ Demo -------------------------------------------------------------------------------- -Please navigate to -`lml/examples `_, -you would find robotchef and its packages. Do the following:: +Do the following:: + $ git clone https://github.com/python-lml/robotchef $ cd robotchef $ python setup.py install @@ -30,6 +29,7 @@ it starts to understand it once you install Chinese cuisine package to complement its knowledge:: + $ git clone https://github.com/python-lml/robotchef_britishcuisine $ cd robotchef_britishcuisine $ python setup.py install diff --git a/docs/source/uml/loading_sequence.uml b/docs/source/uml/loading_sequence.uml new file mode 100644 index 0000000..59cc905 --- /dev/null +++ b/docs/source/uml/loading_sequence.uml @@ -0,0 +1,19 @@ +@startuml + +actor bob +participant robotchef +participant lml +participant robotchef_britishcuisine + +bob -> robotchef : > robotchef "Jacket Potato" +robotchef -> lml : scan for plugins. +lml -> robotchef_britishcuisine : read plugin chain in the module +robotchef_britishcuisine -> lml: I can help with "Jacket Potato" and others. +lml -> robotchef : read the built-in robot_cuisine +robotchef -> lml : built-in chef knows "Portable Battery" +lml --> robotchef : scanning done +robotchef -> lml : get me a plugin that knows "Jacket Potato" +lml -> robotchef : robotchef_britishcuisine.bake.Bake can do +robotchef -> robotchef: make the food +robotchef -> bob : "I can bake Jacket Potato" +@enduml \ No newline at end of file diff --git a/docs/source/uml/robot_chef.uml b/docs/source/uml/robot_chef.uml new file mode 100644 index 0000000..c5601ec --- /dev/null +++ b/docs/source/uml/robot_chef.uml @@ -0,0 +1,9 @@ +@startuml + +Interface Chef + +Chef <|-- Boost +Chef <|-- Fry +Chef <|-- Bake + +@enduml \ No newline at end of file diff --git a/docs/source/uml/robotchef_allinone_lml.uml b/docs/source/uml/robotchef_allinone_lml.uml new file mode 100644 index 0000000..a27a39b --- /dev/null +++ b/docs/source/uml/robotchef_allinone_lml.uml @@ -0,0 +1,25 @@ +@startuml + +package lml { + PluginManager o-- PluginInfo +} + +package robotchef_allinone_lml { + class CuisineManager { + + get_a_plugin() + + raise_exception() + } + interface Chef { + + make() + } + PluginManager <|-- CuisineManager : cuisine + Chef <|-- Boost + Chef <|-- Fry + Chef <|-- Bake + PluginInfo .. Fry + PluginInfo .. Bake + PluginInfo .. Boost +} + + +@enduml \ No newline at end of file diff --git a/docs/source/uml/robotchef_api_crd.uml b/docs/source/uml/robotchef_api_crd.uml new file mode 100644 index 0000000..d2bd3d7 --- /dev/null +++ b/docs/source/uml/robotchef_api_crd.uml @@ -0,0 +1,37 @@ +@startuml + + +package lml { + PluginManager .. PluginInfoChain : registers plugin info + PluginManager o-- PluginInfo + PluginInfoChain -right- PluginInfo +} + +package robotchef_api { + class CuisineManager { + + get_a_plugin() + + raise_exception() + } + interface Chef { + + make() + } + PluginManager <|-- CuisineManager : cuisine + package robotchef.robot_cuisine { + Chef <|-- Boost + PluginInfoChain .. Boost + } +} + +package robotchef_britishcuisine { + Chef <|-- Fry + Chef <|-- Bake + PluginInfoChain .. Fry + PluginInfoChain .. Bake +} + +package robotchef_v2 { +} + +robotchef_v2 +-- robotchef_api + +@enduml \ No newline at end of file diff --git a/docs/source/uml/robotchef_crd.uml b/docs/source/uml/robotchef_crd.uml new file mode 100644 index 0000000..4ee8e2a --- /dev/null +++ b/docs/source/uml/robotchef_crd.uml @@ -0,0 +1,32 @@ +@startuml + +package lml { + PluginManager .. PluginInfoChain : registers plugin info + PluginManager o-- PluginInfo + PluginInfoChain -right- PluginInfo +} + +package robotchef { + class CuisineManager { + + get_a_plugin() + + raise_exception() + } + interface Chef { + + make() + } + PluginManager <|-- CuisineManager : cuisine + package robotchef.robot_cuisine { + Chef <|-- Boost + PluginInfoChain .. Boost + } +} + +package robotchef_britishcuisine { + Chef <|-- Fry + Chef <|-- Bake + PluginInfoChain .. Fry + PluginInfoChain .. Bake +} + + +@enduml \ No newline at end of file diff --git a/examples/README.rst b/examples/README.rst new file mode 100644 index 0000000..8fcd41a --- /dev/null +++ b/examples/README.rst @@ -0,0 +1,12 @@ +READ ME +========= + +A robot chef was created to master the cuisines around the world. It learns faster +than a human because it just needs a plugin to be installed. It consumes less +memory as it load the cuisine knowlege on demand. + +Please note that there are two implementations of the robot chef. One is pure +command line interface(CLI) using lml directly; the other one is CLI using +robotchef_api package which uses lml. The former demonstrates how lml could +be used in a CLI package. The latter illustrates how to construct a pure +python library using lml. diff --git a/lml/__init__.py b/lml/__init__.py index bcdac60..bf0db19 100644 --- a/lml/__init__.py +++ b/lml/__init__.py @@ -8,8 +8,9 @@ :license: New BSD License, see LICENSE for more details """ import logging + +from lml._version import __author__ # noqa: F401 from lml._version import __version__ # noqa: F401 -from lml._version import __author__ # noqa: F401 try: from logging import NullHandler diff --git a/lml/_version.py b/lml/_version.py index 4380c4e..c606527 100644 --- a/lml/_version.py +++ b/lml/_version.py @@ -1,2 +1,2 @@ -__version__ = "0.0.9" +__version__ = "0.1.0" __author__ = "C.W." diff --git a/lml/loader.py b/lml/loader.py index a4cb1ca..0d5482d 100644 --- a/lml/loader.py +++ b/lml/loader.py @@ -6,7 +6,7 @@ and pyinstaller. :func:`~lml.loader.scan_plugins` is expected to be called in the main package of yours at an earliest time of convenience. - :copyright: (c) 2017-2018 by Onni Software Ltd. + :copyright: (c) 2017-2020 by Onni Software Ltd. :license: New BSD License, see LICENSE for more details """ import re diff --git a/lml/plugin.py b/lml/plugin.py index 70629fc..1a49d54 100644 --- a/lml/plugin.py +++ b/lml/plugin.py @@ -22,7 +22,7 @@ can be overridden to help its matching :class:`~lml.plugin.PluginManager` to look itself up. - :copyright: (c) 2017-2018 by Onni Software Ltd. + :copyright: (c) 2017-2020 by Onni Software Ltd. :license: New BSD License, see LICENSE for more details """ import logging @@ -273,6 +273,7 @@ if keywords: self._logger.debug(keywords) __key = key.lower() + if __key in self.registry: for plugin_info in self.registry[__key]: cls = self.dynamic_load_library(plugin_info) @@ -282,8 +283,9 @@ else: break else: - # only library condition coud raise an exception - raise Exception("%s is not installed" % library) + # only library condition could raise an exception + self._logger.debug("%s is not installed" % library) + self.raise_exception(key) self._logger.debug("load %s now for '%s'", cls, key) return cls else: @@ -363,7 +365,10 @@ manager.register_a_plugin(plugin_cls, plugin_info) else: # let's cache it and wait the manager to be registered - log.debug("caching %s", _show_me_your_name(plugin_cls.__name__)) + try: + log.debug("caching %s", _show_me_your_name(plugin_cls.__name__)) + except AttributeError: + log.debug("caching %s", _show_me_your_name(plugin_cls)) CACHED_PLUGIN_INFO[plugin_info.plugin_type].append(plugin_info) diff --git a/lml/utils.py b/lml/utils.py index 45c474d..a9e385c 100644 --- a/lml/utils.py +++ b/lml/utils.py @@ -4,7 +4,7 @@ json utils for dump plugin info class - :copyright: (c) 2017-2018 by Onni Software Ltd. + :copyright: (c) 2017-2020 by Onni Software Ltd. :license: New BSD License, see LICENSE for more details """ import sys @@ -41,9 +41,7 @@ try: return _do_import(plugin_module_name) except ImportError: - log.exception( - "%s is abscent or cannot be imported", plugin_module_name - ) + log.exception("%s is absent or cannot be imported", plugin_module_name) raise diff --git a/lml.egg-info/PKG-INFO b/lml.egg-info/PKG-INFO deleted file mode 100644 index 1d394bb..0000000 --- a/lml.egg-info/PKG-INFO +++ /dev/null @@ -1,163 +0,0 @@ -Metadata-Version: 1.1 -Name: lml -Version: 0.0.9 -Summary: Load me later. A lazy plugin management system. -Home-page: https://github.com/chfw/lml -Author: C.W. -Author-email: wangc_2011@hotmail.com -License: New BSD -Download-URL: https://github.com/chfw/lml/archive/0.0.9.tar.gz -Description: ================================================================================ - lml - Load me later. A lazy plugin management system. - ================================================================================ - - .. image:: https://api.travis-ci.org/chfw/lml.svg - :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/ - - **lml** seamlessly finds the lml based plugins from your current python - environment but loads your plugins on demand. It is designed to support - plugins that have external dependencies, especially bulky and/or - memory hungry ones. lml provides the plugin management system only and the - plugin interface is on your shoulder. - - **lml** enabled applications helps your customers [#f1]_ in two ways: - - #. Your customers could cherry-pick the plugins from pypi per python environment. - They could remove a plugin using `pip uninstall` command. - #. Only the plugins used at runtime gets loaded into computer memory. - - When you would use **lml** to refactor your existing code, it aims to flatten the - complexity and to shrink the size of your bulky python library by - distributing the similar functionalities across its plugins. However, you as - the developer need to do the code refactoring by yourself and lml would lend you a hand. - - .. [#f1] the end developers who uses your library and packages achieve their - objectives. - - Installation - ================================================================================ - - - You can install lml via pip: - - .. code-block:: bash - - $ pip install lml - - - or clone it and install it: - - .. code-block:: bash - - $ git clone https://github.com/chfw/lml.git - $ cd lml - $ python setup.py install - - License - ================================================================================ - - New BSD - - Change log - ================================================================================ - - 0.0.9 - 7/1/2019 - -------------------------------------------------------------------------------- - - Updated - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. `#11 `_: more test contents for - OpenSuse package validation - - 0.0.8 - 4/1/2019 - -------------------------------------------------------------------------------- - - Updated - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. `#9 `_: include tests, docs for - OpenSuse package validation - - 0.0.7 - 17/11/2018 - -------------------------------------------------------------------------------- - - Fixed - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. `#8 `_: get_primary_key will fail when - a module is loaded later - #. deprecated old style plugin scanner: scan_plugins - - 0.0.6 - 07/11/2018 - -------------------------------------------------------------------------------- - - Fixed - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. Revert the version 0.0.5 changes. Raise Import error and log the exception - - 0.0.5 - 06/11/2018 - -------------------------------------------------------------------------------- - - Fixed - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. `#6 `_: Catch and Ignore - ModuleNotFoundError - - 0.0.4 - 07.08.2018 - -------------------------------------------------------------------------------- - - Added - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. `#4 `_: to find plugin names with - different naming patterns - - 0.0.3 - 12/06/2018 - -------------------------------------------------------------------------------- - - Added - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. `dict` can be a pluggable type in addition to `function`, `class` - #. get primary tag of your tag, helping you find out which category of plugins - your tag points to - - 0.0.2 - 23/10/2017 - -------------------------------------------------------------------------------- - - Updated - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. `pyexcel#103 `_: include - LICENSE in tar ball - - 0.0.1 - 30/05/2017 - -------------------------------------------------------------------------------- - - Added - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - #. First release - - -Keywords: python -Platform: UNKNOWN -Classifier: Topic :: Software Development :: Libraries -Classifier: Programming Language :: Python -Classifier: Intended Audience :: Developers -Classifier: Programming Language :: Python :: 2.6 -Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3.3 -Classifier: Programming Language :: Python :: 3.4 -Classifier: Programming Language :: Python :: 3.5 -Classifier: Programming Language :: Python :: 3.6 diff --git a/lml.egg-info/SOURCES.txt b/lml.egg-info/SOURCES.txt deleted file mode 100644 index d69f64f..0000000 --- a/lml.egg-info/SOURCES.txt +++ /dev/null @@ -1,39 +0,0 @@ -CHANGELOG.rst -LICENSE -MANIFEST.in -README.rst -setup.cfg -setup.py -docs/source/allinone_lml_tutorial.rst -docs/source/allinone_tutorial.rst -docs/source/api.rst -docs/source/api_tutorial.rst -docs/source/appendix.rst -docs/source/conf.py -docs/source/design.rst -docs/source/index.rst -docs/source/lml_log.rst -docs/source/lml_tutorial.rst -docs/source/spelling_wordlist.txt -docs/source/tutorial.rst -lml/__init__.py -lml/_version.py -lml/loader.py -lml/plugin.py -lml/utils.py -lml.egg-info/PKG-INFO -lml.egg-info/SOURCES.txt -lml.egg-info/dependency_links.txt -lml.egg-info/not-zip-safe -lml.egg-info/top_level.txt -tests/requirements.txt -tests/test_plugin_info.py -tests/test_plugin_loader.py -tests/test_plugin_manager.py -tests/test_utils.py -tests/sample_plugin/setup.py -tests/sample_plugin/sample_plugin/__init__.py -tests/sample_plugin/sample_plugin/manager.py -tests/sample_plugin/sample_plugin/reader.py -tests/test_plugin/setup.py -tests/test_plugin/pyexcel_test/__init__.py \ No newline at end of file diff --git a/lml.egg-info/dependency_links.txt b/lml.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/lml.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lml.egg-info/not-zip-safe b/lml.egg-info/not-zip-safe deleted file mode 100644 index 8b13789..0000000 --- a/lml.egg-info/not-zip-safe +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lml.egg-info/top_level.txt b/lml.egg-info/top_level.txt deleted file mode 100644 index 87bedb6..0000000 --- a/lml.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -lml diff --git a/lml.yml b/lml.yml new file mode 100644 index 0000000..6acdc76 --- /dev/null +++ b/lml.yml @@ -0,0 +1,20 @@ +name: "lml" +full_name: "Load me later. A lazy plugin management system." +organisation: "python-lml" +author: "C.W." +contact: "wangc_2011@hotmail.com" +company: "Onni Software Ltd." +version: "0.1.0" +current_version: "0.1.0" +release: "0.1.0" +copyright_year: 2017-2020 +license: New BSD +dependencies: [] +test_dependencies: + - lml + - pytest + - pytest-cov +description: "Load me later. A lazy plugin management system." +excluded_github_users: + - chfw + - gitter-badger diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/rnd_requirements.txt b/rnd_requirements.txt new file mode 100644 index 0000000..84372e8 --- /dev/null +++ b/rnd_requirements.txt @@ -0,0 +1 @@ +https://github.com/pyexcel/pyexcel-xls/archive/v0.4.x.zip diff --git a/setup.cfg b/setup.cfg index 45563e7..8c8abfd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,10 +1,4 @@ [metadata] description-file = README.rst - [bdist_wheel] universal = 1 - -[egg_info] -tag_build = -tag_date = 0 - diff --git a/setup.py b/setup.py index c62cafe..fd839e9 100644 --- a/setup.py +++ b/setup.py @@ -1,72 +1,94 @@ #!/usr/bin/env python3 -# Template by pypi-mobans +""" +Template by pypi-mobans +""" + import os import sys import codecs +import locale +import platform from shutil import rmtree from setuptools import Command, setup, find_packages PY2 = sys.version_info[0] == 2 PY26 = PY2 and sys.version_info[1] < 7 - -NAME = 'lml' -AUTHOR = 'C.W.' -VERSION = '0.0.9' -EMAIL = 'wangc_2011@hotmail.com' -LICENSE = 'New BSD' +PY33 = sys.version_info < (3, 4) + +# Work around mbcs bug in distutils. +# http://bugs.python.org/issue10945 +# This work around is only if a project supports Python < 3.4 + +# Work around for locale not being set +try: + lc = locale.getlocale() + pf = platform.system() + if pf != "Windows" and lc == (None, None): + locale.setlocale(locale.LC_ALL, "C.UTF-8") +except (ValueError, UnicodeError, locale.Error): + locale.setlocale(locale.LC_ALL, "en_US.UTF-8") + +NAME = "lml" +AUTHOR = "C.W." +VERSION = "0.1.0" +EMAIL = "wangc_2011@hotmail.com" +LICENSE = "New BSD" DESCRIPTION = ( - 'Load me later. A lazy plugin management system.' + "Load me later. A lazy plugin management system." ) -URL = 'https://github.com/chfw/lml' -DOWNLOAD_URL = '%s/archive/0.0.9.tar.gz' % URL -FILES = ['README.rst', 'CHANGELOG.rst'] +URL = "https://github.com/python-lml/lml" +DOWNLOAD_URL = "%s/archive/0.1.0.tar.gz" % URL +FILES = ["README.rst", "CHANGELOG.rst"] KEYWORDS = [ - 'python', + "python", ] CLASSIFIERS = [ - 'Topic :: Software Development :: Libraries', - 'Programming Language :: Python', - 'Intended Audience :: Developers', - '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', + "Topic :: Software Development :: Libraries", + "Programming Language :: Python", + "Intended Audience :: Developers", + "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", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + ] + INSTALL_REQUIRES = [ ] SETUP_COMMANDS = {} - -PACKAGES = find_packages(exclude=['ez_setup', 'examples', 'tests']) +PACKAGES = find_packages(exclude=["ez_setup", "examples", "tests", "tests.*"]) EXTRAS_REQUIRE = {} # You do not need to read beyond this line -PUBLISH_COMMAND = '{0} setup.py sdist bdist_wheel upload -r pypi'.format( - sys.executable) -GS_COMMAND = ('gs lml v0.0.9 ' + - "Find 0.0.9 in changelog for more details") -NO_GS_MESSAGE = ('Automatic github release is disabled. ' + - 'Please install gease to enable it.') +PUBLISH_COMMAND = "{0} setup.py sdist bdist_wheel upload -r pypi".format(sys.executable) +HERE = os.path.abspath(os.path.dirname(__file__)) + +GS_COMMAND = ("gease lml v0.1.0 " + + "Find 0.1.0 in changelog for more details") +NO_GS_MESSAGE = ("Automatic github release is disabled. " + + "Please install gease to enable it.") UPLOAD_FAILED_MSG = ( 'Upload failed. please run "%s" yourself.' % PUBLISH_COMMAND) -HERE = os.path.abspath(os.path.dirname(__file__)) class PublishCommand(Command): """Support setup.py upload.""" - description = 'Build and publish the package on github and pypi' + description = "Build and publish the package on github and pypi" user_options = [] @staticmethod def status(s): """Prints things in bold.""" - print('\033[1m{0}\033[0m'.format(s)) + print("\033[1m{0}\033[0m".format(s)) def initialize_options(self): pass @@ -76,14 +98,14 @@ def run(self): try: - self.status('Removing previous builds...') - rmtree(os.path.join(HERE, 'dist')) - rmtree(os.path.join(HERE, 'build')) - rmtree(os.path.join(HERE, 'lml.egg-info')) + self.status("Removing previous builds...") + rmtree(os.path.join(HERE, "dist")) + rmtree(os.path.join(HERE, "build")) + rmtree(os.path.join(HERE, "lml.egg-info")) except OSError: pass - self.status('Building Source and Wheel (universal) distribution...') + self.status("Building Source and Wheel (universal) distribution...") run_status = True if has_gease(): run_status = os.system(GS_COMMAND) == 0 @@ -91,15 +113,14 @@ self.status(NO_GS_MESSAGE) if run_status: if os.system(PUBLISH_COMMAND) != 0: - self.status(UPLOAD_FAILED_MSG % PUBLISH_COMMAND) + self.status(UPLOAD_FAILED_MSG) sys.exit() SETUP_COMMANDS.update({ - 'publish': PublishCommand + "publish": PublishCommand }) - def has_gease(): """ @@ -126,7 +147,7 @@ def read(afile): """Read a file into setup""" the_relative_file = os.path.join(HERE, afile) - with codecs.open(the_relative_file, 'r', 'utf-8') as opened_file: + with codecs.open(the_relative_file, "r", "utf-8") as opened_file: content = filter_out_test_code(opened_file) content = "".join(list(content)) return content @@ -135,11 +156,11 @@ def filter_out_test_code(file_handle): found_test_code = False for line in file_handle.readlines(): - if line.startswith('.. testcode:'): + if line.startswith(".. testcode:"): found_test_code = True continue if found_test_code is True: - if line.startswith(' '): + if line.startswith(" "): continue else: empty_line = line.strip() @@ -149,15 +170,16 @@ found_test_code = False yield line else: - for keyword in ['|version|', '|today|']: + for keyword in ["|version|", "|today|"]: if keyword in line: break else: yield line -if __name__ == '__main__': +if __name__ == "__main__": setup( + test_suite="tests", name=NAME, author=AUTHOR, version=VERSION, @@ -169,7 +191,7 @@ license=LICENSE, keywords=KEYWORDS, extras_require=EXTRAS_REQUIRE, - tests_require=['nose'], + tests_require=["nose"], install_requires=INSTALL_REQUIRES, packages=PACKAGES, include_package_data=True, diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..8527177 --- /dev/null +++ b/test.sh @@ -0,0 +1,5 @@ +pip freeze +cd tests/test_plugin +python setup.py install +cd - +pytest tests --verbosity=3 --cov=lml --doctest-glob=*.rst && flake8 . --exclude=.moban.d,docs,setup.py --builtins=unicode,xrange,long diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/requirements.txt b/tests/requirements.txt index 8c52119..05bb89b 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,5 +1,6 @@ mock -nose +pytest +pytest-cov codecov coverage flake8 diff --git a/tests/sample_plugin/sample_plugin/__init__.py b/tests/sample_plugin/sample_plugin/__init__.py index e062d6b..c3eac13 100644 --- a/tests/sample_plugin/sample_plugin/__init__.py +++ b/tests/sample_plugin/sample_plugin/__init__.py @@ -1,4 +1,3 @@ from lml.registry import PluginInfoChain - __test_plugins__ = PluginInfoChain(__name__).add_a_plugin("test_io2", "reader") diff --git a/tests/test_plugin/pyexcel_test/__init__.py b/tests/test_plugin/pyexcel_test/__init__.py index e5c2055..d11cfe6 100644 --- a/tests/test_plugin/pyexcel_test/__init__.py +++ b/tests/test_plugin/pyexcel_test/__init__.py @@ -1,4 +1,3 @@ from lml.plugin import PluginInfoChain - __test_plugins__ = PluginInfoChain(__name__).add_a_plugin("test_io", "x") diff --git a/tests/test_plugin_info.py b/tests/test_plugin_info.py index c81f4c7..d9ecffa 100644 --- a/tests/test_plugin_info.py +++ b/tests/test_plugin_info.py @@ -1,7 +1,6 @@ import json from lml.plugin import PluginInfo -from nose.tools import eq_ def test_plugin_info(): @@ -18,7 +17,7 @@ "plugin_type": "renderer", "custom": "property", } - eq_(json.loads(info.__repr__()), expected) + assert json.loads(info.__repr__()) == expected def test_module_name_scenario_2(): @@ -27,4 +26,4 @@ info = PluginInfo("renderer", custom="property") info.cls = TestClass2 - eq_(info.module_name, "test_plugin_info") + assert info.module_name == "tests.test_plugin_info" diff --git a/tests/test_plugin_loader.py b/tests/test_plugin_loader.py index 356644b..968b9cb 100644 --- a/tests/test_plugin_loader.py +++ b/tests/test_plugin_loader.py @@ -1,5 +1,4 @@ from mock import patch -from nose.tools import eq_ @patch("pkgutil.get_importer") @@ -10,7 +9,7 @@ module_names = scan_from_pyinstaller("pyexcel_", "path") expected = ["pyexcel_io", "pyexcel_xls"] - eq_(sorted(list(module_names)), sorted(expected)) + assert sorted(list(module_names)) == sorted(expected) @patch("pkgutil.get_importer") @@ -21,7 +20,7 @@ module_names = scan_from_pyinstaller("^.+cel_.+$", "path") expected = ["pyexcel_io", "pyexcel_xls"] - eq_(sorted(list(module_names)), sorted(expected)) + assert sorted(list(module_names)) == sorted(expected) @patch("pkgutil.get_importer") @@ -37,8 +36,8 @@ from lml.plugin import CACHED_PLUGIN_INFO info = CACHED_PLUGIN_INFO["test_io"][0] - eq_(info.plugin_type, "test_io") - eq_(info.absolute_import_path, "pyexcel_test.x") + assert info.plugin_type == "test_io" + assert info.absolute_import_path == "pyexcel_test.x" @patch("pkgutil.get_importer") @@ -57,8 +56,8 @@ from lml.plugin import CACHED_PLUGIN_INFO info = CACHED_PLUGIN_INFO["test_io"][0] - eq_(info.plugin_type, "test_io") - eq_(info.absolute_import_path, "pyexcel_test.x") + assert info.plugin_type == "test_io" + assert info.absolute_import_path == "pyexcel_test.x" @patch("pkgutil.get_importer") @@ -73,7 +72,7 @@ from lml.loader import scan_plugins scan_plugins("pyexcel_", ".", ["pyexcel_io"]) - assert mocked_load_me_later.called is False + assert not mocked_load_me_later.called @patch("pkgutil.get_importer") @@ -88,7 +87,7 @@ from lml.loader import scan_plugins scan_plugins("pyexcel_", ".") - assert mocked_load_me_later.called is False + assert not mocked_load_me_later.called @patch("pkgutil.get_importer") @@ -103,4 +102,4 @@ from lml.loader import scan_plugins scan_plugins("test_", ".", ["pyexcel_io"]) - assert mocked_load_me_later.called is False + assert not mocked_load_me_later.called diff --git a/tests/test_plugin_manager.py b/tests/test_plugin_manager.py index 49ea972..b82b436 100644 --- a/tests/test_plugin_manager.py +++ b/tests/test_plugin_manager.py @@ -1,4 +1,3 @@ -from mock import patch from lml.plugin import ( PLUG_IN_MANAGERS, CACHED_PLUGIN_INFO, @@ -6,7 +5,9 @@ PluginManager, _show_me_your_name, ) -from nose.tools import eq_, raises + +from mock import patch +from pytest import raises def test_plugin_manager(): @@ -32,28 +33,40 @@ plugin_info = make_me_a_plugin_info(test_plugin) manager.load_me_later(plugin_info) actual = manager.load_me_now(test_plugin) - eq_(actual, custom_class) - eq_(manager.tag_groups, {"my plugin": "my plugin"}) - eq_(plugin_info, manager.registry["my plugin"][0]) + assert actual == custom_class + assert manager.tag_groups == {"my plugin": "my plugin"} + assert plugin_info == manager.registry["my plugin"][0] -@raises(Exception) +@patch("lml.plugin.do_import_class") +def test_load_me_now_with_known_missing_library(mock_import): + custom_class = PluginInfo + mock_import.return_value = custom_class + test_plugin = "my plugin" + manager = PluginManager(test_plugin) + plugin_info = make_me_a_plugin_info(test_plugin) + manager.load_me_later(plugin_info) + with raises(Exception): + manager.load_me_now(test_plugin, library='alien') + + @patch("lml.plugin.do_import_class") def test_load_me_now_exception(mock_import): custom_class = PluginInfo mock_import.return_value = custom_class test_plugin = "my plugin" - manager = PluginManager(test_plugin) - plugin_info = make_me_a_plugin_info("my") - manager.load_me_later(plugin_info) - manager.load_me_now("my", "my special library") + with raises(Exception): + manager = PluginManager(test_plugin) + plugin_info = make_me_a_plugin_info("my") + manager.load_me_later(plugin_info) + manager.load_me_now("my", "my special library") -@raises(Exception) def test_load_me_now_no_key_found(): test_plugin = "my plugin" - manager = PluginManager(test_plugin) - manager.load_me_now("my", custom_property="here") + with raises(Exception): + manager = PluginManager(test_plugin) + manager.load_me_now("my", custom_property="here") @patch("lml.plugin.do_import_class") @@ -64,7 +77,7 @@ manager = PluginManager(test_plugin) plugin_info = make_me_a_plugin_info(test_plugin) manager.dynamic_load_library(plugin_info) - eq_(custom_obj, plugin_info.cls) + assert custom_obj == plugin_info.cls @patch("lml.plugin.do_import_class") @@ -87,9 +100,9 @@ manager = PluginManager(test_plugin) plugin_info = make_me_a_plugin_info("my") manager.register_a_plugin(TestClass, plugin_info) - eq_(plugin_info.cls, TestClass) - eq_(manager.registry["my"][0], plugin_info) - eq_(manager.tag_groups, {"my": "my"}) + assert plugin_info.cls == TestClass + assert manager.registry["my"][0] == plugin_info + assert manager.tag_groups == {"my": "my"} def test_get_a_plugin(): @@ -121,11 +134,11 @@ assert list(manager.registry.keys()) == [test_plugin] -@raises(ImportError) def test_do_import_cls_error(): from lml.plugin import do_import_class - do_import_class("non.exist.class") + with raises(ImportError): + do_import_class("non.exist.class") def test_register_a_plugin_function_1(): @@ -157,7 +170,7 @@ pass pk = manager.get_primary_key("key 1") - eq_(pk, "primary key") + assert pk == "primary key" def test_dict_as_plugin_payload(): @@ -167,7 +180,7 @@ plugin(dict(B=1)) instance = manager.load_me_now("key 1") - eq_(instance, dict(B=1)) + assert instance == dict(B=1) def test_show_me_your_name(): @@ -175,7 +188,7 @@ pass name = _show_me_your_name(Test) - eq_(name, "Test") + assert name == "Test" name2 = _show_me_your_name(dict(A=1)) assert "dict" in name2 diff --git a/tests/test_utils.py b/tests/test_utils.py index 0f4a605..e6820c9 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,8 @@ -from mock import patch from lml.utils import do_import, json_dumps from lml.plugin import PluginManager -from nose.tools import eq_, raises + +from mock import patch +from pytest import raises def test_json_dumps(): @@ -14,28 +15,30 @@ def test_do_import(): - import pyexcel_test + import isort - pyexcel_test_package = do_import("pyexcel_test") - eq_(pyexcel_test_package, pyexcel_test) + test_package = do_import("isort") + assert test_package == isort def test_do_import_2(): import lml.plugin as plugin themodule = do_import("lml.plugin") - eq_(plugin, themodule) + assert plugin == themodule -@raises(ImportError) @patch("lml.utils.log.exception") def test_do_import_error(mock_exception): - do_import("non.exist") - mock_exception.assert_called_with("No module named 'non'") + with raises(ImportError): + do_import("non.exist") + mock_exception.assert_called_with( + "%s is absent or cannot be imported", "non.exist" + ) def test_do_import_cls(): from lml.utils import do_import_class manager = do_import_class("lml.plugin.PluginManager") - eq_(manager, PluginManager) + assert manager == PluginManager