This is an automated email from the git hooks/post-receive script. johanvdw-guest pushed a commit to branch master in repository python-click-plugins.
commit 63573a3cd2c5da935ac132e78d629329eafda77e Author: Johan Van de Wauw <[email protected]> Date: Tue Aug 4 14:12:07 2015 +0200 Imported Upstream version 1.0 --- .gitignore | 60 ++++++++ .travis.yml | 18 +++ AUTHORS.txt | 5 + CHANGES.txt | 8 ++ LICENSE.txt | 29 ++++ MANIFEST.in | 6 + README.rst | 187 +++++++++++++++++++++++++ click_plugins/__init__.py | 60 ++++++++ click_plugins/core.py | 89 ++++++++++++ example/PrintIt/README.rst | 5 + example/PrintIt/printit/__init__.py | 3 + example/PrintIt/printit/cli.py | 49 +++++++ example/PrintIt/printit/core.py | 3 + example/PrintIt/setup.py | 20 +++ example/PrintItBold/README.rst | 5 + example/PrintItBold/printit_bold/__init__.py | 3 + example/PrintItBold/printit_bold/core.py | 19 +++ example/PrintItBold/setup.py | 20 +++ example/PrintItStyle/README.rst | 4 + example/PrintItStyle/printit_style/__init__.py | 3 + example/PrintItStyle/printit_style/core.py | 46 ++++++ example/PrintItStyle/setup.py | 21 +++ example/README.rst | 155 ++++++++++++++++++++ setup.py | 66 +++++++++ tests/__init__.py | 1 + tests/conftest.py | 8 ++ tests/test_plugins.py | 140 ++++++++++++++++++ 27 files changed, 1033 insertions(+) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72ba0c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +venv/ +venv2/ +venv3/ +build/ +develop-eggs/ +dist/ +downloads/ +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 +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Intellij PyCharm +.idea/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a2c4a8c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: python + +python: + - "2.7" + - "3.3" + - "3.4" + - "pypy" + - "pypy3" + +install: + - pip install coveralls + - pip install -e .[dev] + +script: + - py.test tests --cov click_plugins --cov-report term-missing + +after_success: + - coveralls diff --git a/AUTHORS.txt b/AUTHORS.txt new file mode 100644 index 0000000..17b68ca --- /dev/null +++ b/AUTHORS.txt @@ -0,0 +1,5 @@ +Authors +======= + +Kevin Wurster <[email protected]> +Sean Gillies <[email protected]> diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100644 index 0000000..4f9ecbb --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,8 @@ +Changelog +========= + + +1.0 - 2015-07-20 +---------------- + +Initial release. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..3be440d --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,29 @@ +New BSD License + +Copyright (c) 2015, Kevin D. Wurster +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither click-contrib nor the names of its contributors may not be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..8c0eecf --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include AUTHORS.txt +include CHANGES.txt +include LICENSE.txt +include MANIFEST.in +include README.rst +recursive-include tests *.py diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..074ef59 --- /dev/null +++ b/README.rst @@ -0,0 +1,187 @@ +============= +click-plugins +============= + +.. image:: https://travis-ci.org/click-contrib/click-plugins.svg?branch=master + :target: https://travis-ci.org/click-contrib/click-plugins?branch=master + +.. image:: https://coveralls.io/repos/click-contrib/click-plugins/badge.svg?branch=master&service=github + :target: https://coveralls.io/github/click-contrib/click-plugins?branch=master + +An extension module for `click <https://github.com/mitsuhiko/click>`_ to register +external CLI commands via setuptools entry-points. + + +Why? +---- + +Lets say you develop a commandline interface and someone requests a new feature +that is absolutely related to your project but would have negative consequences +like additional dependencies, major refactoring, or maybe its just too domain +specific to be supported directly. Rather than developing a separate standalone +utility you could offer up a `setuptools entry point <https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins>`_ +that allows others to use your commandline utility as a home for their related +sub-commands. You get to choose where these sub-commands or sub-groups CAN be +registered but the plugin developer gets to choose they ARE registered. You +could have all plugins register alongside the core commands, in a special +sub-group, across multiple sub-groups, or some combination. + + +Enabling Plugins +---------------- + +For a more detailed example see the `examples <https://github.com/click-contrib/click-plugins/tree/master/examples>`_ section. + +The only requirement is decorating ``click.group()`` with ``click_plugins.with_plugins()`` +which handles attaching external commands and groups. In this case the core CLI developer +registers CLI plugins from ``core_package.cli_plugins``. + +.. code-block:: python + + from pkg_resources import iter_entry_points + + import click + from click_plugins import with_plugins + + + @with_plugins(iter_entry_points('core_package.cli_plugins')) + @click.group() + def cli(): + """Commandline interface for yourpackage.""" + + @cli.command() + def subcommand(): + """Subcommand that does something.""" + + +Developing Plugins +------------------ + +Plugin developers need to register their sub-commands or sub-groups to an +entry-point in their ``setup.py`` that is loaded by the core package. + +.. code-block:: python + + from setuptools import setup + + setup( + name='yourscript', + version='0.1', + py_modules=['yourscript'], + install_requires=[ + 'click', + ], + entry_points=''' + [core_package.cli_plugins] + cool_subcommand=yourscript.cli:cool_subcommand + another_subcommand=yourscript.cli:another_subcommand + ''', + ) + + +Broken and Incompatible Plugins +------------------------------- + +Any sub-command or sub-group that cannot be loaded is caught and converted to +a ``click_plugins.core.BrokenCommand()`` rather than just crashing the entire +CLI. The short-help is converted to a warning message like: + +.. code-block:: console + + Warning: could not load plugin. See ``<CLI> <command/group> --help``. + +and if the sub-command or group is executed the entire traceback is printed. + + +Best Practices and Extra Credit +------------------------------- + +Opening a CLI to plugins encourages other developers to independently extend +functionality independently but their is no guarantee these new features will +be "on brand". Plugin developers are almost certainly already using features +in the core package the CLI belongs to so defining commonly used arguments and +options in one place lets plugin developers reuse these flags to produce a more +cohesive CLI. If the CLI is simple maybe just define them at the top of +``yourpackage/cli.py`` or for more complex packages something like +``yourpackage/cli/options.py``. These common options need to be easy to find +and be well documented so that plugin developers know what variable to give to +their sub-command's function and what object they can expect to receive. Don't +forget to document non-obvious callbacks. + +Keep in mind that plugin developers also have access to the parent group's +``ctx.obj``, which is very useful for passing things like verbosity levels or +config values around to sub-commands. + +Here's some code that sub-commands could re-use: + +.. code-block:: python + + from multiprocessing import cpu_count + + import click + + jobs_opt = click.option( + '-j', '--jobs', metavar='CORES', type=click.IntRange(min=1, max=cpu_count()), default=1, + show_default=True, help="Process data across N cores." + ) + +Plugin developers can access this with: + +.. code-block:: python + + import click + import parent_cli_package.cli.options + + + @click.command() + @parent_cli_package.cli.options.jobs_opt + def subcommand(jobs): + """I do something domain specific.""" + + +Installation +------------ + +With ``pip``: + +.. code-block:: console + + $ pip install click-plugins + +From source: + +.. code-block:: console + + $ git clone https://github.com/click-contrib/click-plugins.git + $ cd click-plugins + $ python setup.py install + + +Developing +---------- + +.. code-block:: console + + $ git clone https://github.com/click-contrib/click-plugins.git + $ cd click-plugins + $ virtualenv venv && source venv/bin/activate + $ pip install -e .[dev] + $ py.test tests --cov click_plugins --cov-report term-missing + + +Changelog +--------- + +See ``CHANGES.txt`` + + +Authors +------- + +See ``AUTHORS.txt`` + + +License +------- + +See ``LICENSE.txt`` diff --git a/click_plugins/__init__.py b/click_plugins/__init__.py new file mode 100644 index 0000000..0c3860f --- /dev/null +++ b/click_plugins/__init__.py @@ -0,0 +1,60 @@ +""" +An extension module for click to enable registering CLI commands via setuptools +entry-points. + + + from pkg_resources import iter_entry_points + + import click + from click_plugins import with_plugins + + + @with_plugins(iter_entry_points('entry_point.name')) + @click.group() + def cli(): + '''Commandline interface for something.''' + + @cli.command() + @click.argument('arg') + def subcommand(arg): + '''A subcommand for something else''' +""" + + +from .core import with_plugins + + +__version__ = '1.0' +__author__ = 'Kevin Wurster, Sean Gillies' +__email__ = '[email protected], [email protected]' +__source__ = 'https://github.com/click-contrib/click-plugins' +__license__ = ''' +New BSD License + +Copyright (c) 2015, Kevin D. Wurster, Sean C. Gillies +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* The names of its contributors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''' diff --git a/click_plugins/core.py b/click_plugins/core.py new file mode 100644 index 0000000..4077b12 --- /dev/null +++ b/click_plugins/core.py @@ -0,0 +1,89 @@ +""" +Core components for click_plugins +""" + + +import click + +import os +import sys +import traceback + + +def with_plugins(plugins): + + """ + A decorator to register external CLI commands to an instance of + `click.Group()`. + + Parameters + ---------- + plugins : iter + An iterable producing one `pkg_resources.EntryPoint()` per iteration. + attrs : **kwargs, optional + Additional keyword arguments for instantiating `click.Group()`. + + Returns + ------- + click.Group() + """ + + def decorator(group): + if not isinstance(group, click.Group): + raise TypeError("Plugins can only be attacked to an instance of click.Group()") + + for entry_point in plugins or (): + try: + group.add_command(entry_point.load()) + except Exception: + # Catch this so a busted plugin doesn't take down the CLI. + # Handled by registering a dummy command that does nothing + # other than explain the error. + group.add_command(BrokenCommand(entry_point.name)) + + return group + + return decorator + + +class BrokenCommand(click.Command): + + """ + Rather than completely crash the CLI when a broken plugin is loaded, this + class provides a modified help message informing the user that the plugin is + broken and they should contact the owner. If the user executes the plugin + or specifies `--help` a traceback is reported showing the exception the + plugin loader encountered. + """ + + def __init__(self, name): + + """ + Define the special help messages after instantiating a `click.Command()`. + """ + + click.Command.__init__(self, name) + + util_name = os.path.basename(sys.argv and sys.argv[0] or __file__) + + if os.environ.get('CLICK_PLUGINS_HONESTLY'): # pragma no cover + icon = u'\U0001F4A9' + else: + icon = u'\u2020' + + self.help = ( + "\nWarning: entry point could not be loaded. Contact " + "its author for help.\n\n\b\n" + + traceback.format_exc()) + self.short_help = ( + icon + " Warning: could not load plugin. See `%s %s --help`." + % (util_name, self.name)) + + def invoke(self, ctx): + + """ + Print the traceback instead of doing nothing. + """ + + click.echo(self.help, color=ctx.color) + ctx.exit(1) diff --git a/example/PrintIt/README.rst b/example/PrintIt/README.rst new file mode 100644 index 0000000..4aea574 --- /dev/null +++ b/example/PrintIt/README.rst @@ -0,0 +1,5 @@ +PrintIt +======= + +This represents a core package with a CLI that registers external plugins. All +it does is print stuff. diff --git a/example/PrintIt/printit/__init__.py b/example/PrintIt/printit/__init__.py new file mode 100644 index 0000000..ee810b0 --- /dev/null +++ b/example/PrintIt/printit/__init__.py @@ -0,0 +1,3 @@ +""" +Tools for printing things +""" diff --git a/example/PrintIt/printit/cli.py b/example/PrintIt/printit/cli.py new file mode 100644 index 0000000..d6b327d --- /dev/null +++ b/example/PrintIt/printit/cli.py @@ -0,0 +1,49 @@ +""" +Commandline interface for PrintIt +""" + + +from pkg_resources import iter_entry_points + +import click +from click_plugins import with_plugins + + +@with_plugins(iter_entry_points('printit.plugins')) [email protected]() +def cli(): + + """ + Format and print file contents. + + \b + For example: + \b + $ cat README.rst | printit lower + """ + + [email protected]() [email protected]('infile', type=click.File('r'), default='-') [email protected]('outfile', type=click.File('w'), default='-') +def upper(infile, outfile): + + """ + Convert to upper case. + """ + + for line in infile: + outfile.write(line.upper()) + + [email protected]() [email protected]('infile', type=click.File('r'), default='-') [email protected]('outfile', type=click.File('w'), default='-') +def lower(infile, outfile): + + """ + Convert to lower case. + """ + + for line in infile: + outfile.write(line.lower()) diff --git a/example/PrintIt/printit/core.py b/example/PrintIt/printit/core.py new file mode 100644 index 0000000..66e6f2f --- /dev/null +++ b/example/PrintIt/printit/core.py @@ -0,0 +1,3 @@ +""" +Some other file that does other stuff. +""" diff --git a/example/PrintIt/setup.py b/example/PrintIt/setup.py new file mode 100755 index 0000000..0a6a874 --- /dev/null +++ b/example/PrintIt/setup.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + + +""" +Setup script for `PrintIt` +""" + + +from setuptools import setup + + +setup( + name='PrintIt', + version='0.1dev0', + packages=['printit'], + entry_points=''' + [console_scripts] + printit=printit.cli:cli + ''' +) diff --git a/example/PrintItBold/README.rst b/example/PrintItBold/README.rst new file mode 100644 index 0000000..a8b8bf4 --- /dev/null +++ b/example/PrintItBold/README.rst @@ -0,0 +1,5 @@ +PrintItBold +=========== + +This plugin should add bold styling to ``PrintIt`` but there is a typo in the +entry point section of the ``setup.py`` that prevents the plugin from loading. diff --git a/example/PrintItBold/printit_bold/__init__.py b/example/PrintItBold/printit_bold/__init__.py new file mode 100644 index 0000000..7012341 --- /dev/null +++ b/example/PrintItBold/printit_bold/__init__.py @@ -0,0 +1,3 @@ +""" +A CLI plugin for `PrintIt` that adds bold text. +""" diff --git a/example/PrintItBold/printit_bold/core.py b/example/PrintItBold/printit_bold/core.py new file mode 100644 index 0000000..84e6a1b --- /dev/null +++ b/example/PrintItBold/printit_bold/core.py @@ -0,0 +1,19 @@ +""" +Add bold styling to `printit` +""" + + +import click + + [email protected]() [email protected]('infile', type=click.File('r'), default='-') [email protected]('outfile', type=click.File('w'), default='-') +def bold(infile, outfile): + + """ + Make text bold. + """ + + for line in infile: + click.secho(line, bold=True, file=outfile) diff --git a/example/PrintItBold/setup.py b/example/PrintItBold/setup.py new file mode 100755 index 0000000..15f9c38 --- /dev/null +++ b/example/PrintItBold/setup.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + + +""" +Setup script for `PrintItBold` +""" + + +from setuptools import setup + + +setup( + name='PrintItBold', + version='0.1dev0', + packages=['printit_bold'], + entry_points=''' + [printit.plugins] + bold=printit_bold.core:bolddddddddddd + ''' +) diff --git a/example/PrintItStyle/README.rst b/example/PrintItStyle/README.rst new file mode 100644 index 0000000..3881cad --- /dev/null +++ b/example/PrintItStyle/README.rst @@ -0,0 +1,4 @@ +PrintItStyle +============ + +A plugin for ``PrintIt`` that adds commands for text styling. diff --git a/example/PrintItStyle/printit_style/__init__.py b/example/PrintItStyle/printit_style/__init__.py new file mode 100644 index 0000000..bcba816 --- /dev/null +++ b/example/PrintItStyle/printit_style/__init__.py @@ -0,0 +1,3 @@ +""" +A CLI plugin for `PrintIt` that adds styling options. +""" diff --git a/example/PrintItStyle/printit_style/core.py b/example/PrintItStyle/printit_style/core.py new file mode 100644 index 0000000..311f0f1 --- /dev/null +++ b/example/PrintItStyle/printit_style/core.py @@ -0,0 +1,46 @@ +""" +Core components for `PrintItStyle` +""" + + +import click + + +COLORS = ( + 'black', + 'red', + 'green', + 'yellow', + 'blue', + 'magenta', + 'cyan', + 'white', +) + + [email protected]() [email protected]('infile', type=click.File('r'), default='-') [email protected]('outfile', type=click.File('w'), default='-') [email protected]('-c', '--color', type=click.Choice(COLORS), required=True) +def background(infile, outfile, color): + + """ + Add a background color. + """ + + for line in infile: + click.secho(line, file=outfile, color=color) + + [email protected]() [email protected]('infile', type=click.File('r'), default='-') [email protected]('outfile', type=click.File('w'), default='-') [email protected]('-c', '--color', type=click.Choice(COLORS), required=True) +def color(infile, outfile, color): + + """ + Add color to text. + """ + + for line in infile: + click.echo(line, color=color, file=outfile) diff --git a/example/PrintItStyle/setup.py b/example/PrintItStyle/setup.py new file mode 100755 index 0000000..69e8961 --- /dev/null +++ b/example/PrintItStyle/setup.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + + +""" +Setup script for `PrintItStyle` +""" + + +from setuptools import setup + + +setup( + name='PrintItStyle', + version='0.1dev0', + packages=['printit_style'], + entry_points=''' + [printit.plugins] + background=printit_style.core:background + color=printit_style.core:color + ''' +) diff --git a/example/README.rst b/example/README.rst new file mode 100644 index 0000000..5c8861d --- /dev/null +++ b/example/README.rst @@ -0,0 +1,155 @@ +Plugin Example +============== + +A sample package that loads CLI plugins from another package. + + +Contents +-------- + +* ``PrintIt`` - The core package. +* ``PrintItStyle`` - An external plugin for ``PrintIt``'s CLI that adds styling options. +* ``PrintItBold`` - A broken plugin that is should add a command to create bold text, but an error in its ``setup.py`` causes it to not work. + + +Workflow +-------- + +From this directory, install the main package (the slash is mandatory): + +.. code-block:: console + + $ pip install PrintIt/ + +And run the commandline utility to see the usage: + +.. code-block:: console + + $ printit + Usage: printit [OPTIONS] COMMAND [ARGS]... + + Format and print file contents. + + For example: + + $ cat README.rst | printit lower + + Options: + --help Show this message and exit. + + Commands: + lower Convert to lower case. + upper Convert to upper case. + + +Try running ``cat README.rst | printit upper`` to convert this file to upper-case. + +The ``PrintItStyle`` directory is an external CLI plugin that is compatible with +``printit``. In this case ``PrintItStyle`` adds styling options to the ``printit`` +utility. + +Install it (don't forget the slash): + +.. code-block:: console + + $ pip install PrintItStyle/ + +And get the ``printit`` usage again, now with two additional commands: + +.. code-block:: console + + $ printit + Usage: printit [OPTIONS] COMMAND [ARGS]... + + Format and print file contents. + + For example: + + $ cat README.rst | printit lower + + Options: + --help Show this message and exit. + + Commands: + background Add a background color. + color Add color to text. + lower Convert to lower case. + upper Convert to upper case. + + +Broken Plugins +-------------- + +Plugins that trigger an exception on load are flagged in the usage and the full +traceback can be viewed by executing the command. + +Install the included broken plugin, which we expect to give us a bold styling option: + +.. code-block:: console + + $ pip install BrokenPlugin/ + +And look at the ``printit`` usage again - notice the icon next to ``bold``: + +.. code-block:: console + + $ printit + Usage: printit [OPTIONS] COMMAND [ARGS]... + + Format and print file contents. + + For example: + + $ cat README.rst | printit lower + + Options: + --help Show this message and exit. + + Commands: + background Add a background color. + bold † Warning: could not load plugin. See `printit bold --help`. + color Add color to text. + lower Convert to lower case. + upper Convert to upper case. + +Executing ``printit bold`` reveals the full traceback: + +.. code-block:: console + + $ printit bold + + Warning: entry point could not be loaded. Contact its author for help. + + Traceback (most recent call last): + File "/Users/wursterk/github/click/venv/lib/python3.4/site-packages/pkg_resources/__init__.py", line 2353, in resolve + return functools.reduce(getattr, self.attrs, module) + AttributeError: 'module' object has no attribute 'bolddddddddddd' + + During handling of the above exception, another exception occurred: + + Traceback (most recent call last): + File "/Users/wursterk/github/click/click/decorators.py", line 145, in decorator + obj.add_command(entry_point.load()) + File "/Users/wursterk/github/click/venv/lib/python3.4/site-packages/pkg_resources/__init__.py", line 2345, in load + return self.resolve() + File "/Users/wursterk/github/click/venv/lib/python3.4/site-packages/pkg_resources/__init__.py", line 2355, in resolve + raise ImportError(str(exc)) + ImportError: 'module' object has no attribute 'bolddddddddddd' + +In this case the error is in the broken plugin's ``setup.py``. Note the typo +in the ``entry_points`` section. + +.. code-block:: python + + from setuptools import setup + + + setup( + name='PrintItBold', + version='0.1dev0', + packages=['printit_bold'], + entry_points=''' + [printit.plugins] + bold=printit_bold.core:bolddddddddddd + ''' + ) diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..902cb33 --- /dev/null +++ b/setup.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + + +""" +Setup script for click-plugins +""" + + +import os + +from setuptools import find_packages +from setuptools import setup + + +with open('README.rst') as f: + long_desc = f.read().strip() + + +version = None +author = None +email = None +source = None +with open(os.path.join('click_plugins', '__init__.py')) as f: + for line in f: + if line.strip().startswith('__version__'): + version = line.split('=')[1].strip().replace('"', '').replace("'", '') + elif line.strip().startswith('__author__'): + author = line.split('=')[1].strip().replace('"', '').replace("'", '') + elif line.strip().startswith('__email__'): + email = line.split('=')[1].strip().replace('"', '').replace("'", '') + elif line.strip().startswith('__source__'): + source = line.split('=')[1].strip().replace('"', '').replace("'", '') + elif None not in (version, author, email, source): + break + + +setup( + name='click-plugins', + author=author, + author_email=email, + classifiers=[ + 'Topic :: Utilities', + 'Intended Audience :: Developers', + 'Development Status :: 5 - Production/Stable', + 'License :: OSI Approved :: BSD License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + ], + description="An extension module for click to enable registering CLI commands " + "via setuptools entry-points.", + extras_require={ + 'dev': [ + 'pytest', + 'pytest-cov' + ], + }, + include_package_data=True, + install_requires=['click>=3.0'], + keywords='click plugin setuptools entry-point', + license="New BSD", + long_description=long_desc, + packages=find_packages(), + url=source, + version=version, + zip_safe=True +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..3c1c95d --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# This file is required for some of the tests of Python 2 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..3aac933 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +from click.testing import CliRunner + +import pytest + + [email protected](scope='function') +def runner(request): + return CliRunner() diff --git a/tests/test_plugins.py b/tests/test_plugins.py new file mode 100644 index 0000000..be899cb --- /dev/null +++ b/tests/test_plugins.py @@ -0,0 +1,140 @@ +from pkg_resources import EntryPoint +from pkg_resources import iter_entry_points +from pkg_resources import working_set + +import click +from click_plugins import with_plugins +import pytest + + +# Create a few CLI commands for testing [email protected]() [email protected]('arg') +def cmd1(arg): + """Test command 1""" + click.echo('passed') + [email protected]() [email protected]('arg') +def cmd2(arg): + """Test command 2""" + click.echo('passed') + + +# Manually register plugins in an entry point and put broken plugins in a +# different entry point. + +# The `DistStub()` class gets around an exception that is raised when +# `entry_point.load()` is called. By default `load()` has `requires=True` +# which calls `dist.requires()` and the `click.group()` decorator +# doesn't allow us to change this. Because we are manually registering these +# plugins the `dist` attribute is `None` so we can just create a stub that +# always returns an empty list since we don't have any requirements. A full +# `pkg_resources.Distribution()` instance is not needed because there isn't +# a package installed anywhere. +class DistStub(object): + def requires(self, *args): + return [] + +working_set.by_key['click']._ep_map = { + '_test_click_plugins.test_plugins': { + 'cmd1': EntryPoint.parse( + 'cmd1=tests.test_plugins:cmd1', dist=DistStub()), + 'cmd2': EntryPoint.parse( + 'cmd2=tests.test_plugins:cmd2', dist=DistStub()) + }, + '_test_click_plugins.broken_plugins': { + 'before': EntryPoint.parse( + 'before=tests.broken_plugins:before', dist=DistStub()), + 'after': EntryPoint.parse( + 'after=tests.broken_plugins:after', dist=DistStub()), + 'do_not_exist': EntryPoint.parse( + 'do_not_exist=tests.broken_plugins:do_not_exist', dist=DistStub()) + } +} + + +# Main CLI groups - one with good plugins attached and the other broken +@with_plugins(iter_entry_points('_test_click_plugins.test_plugins')) [email protected]() +def good_cli(): + """Good CLI group.""" + pass + +@with_plugins(iter_entry_points('_test_click_plugins.broken_plugins')) [email protected]() +def broken_cli(): + """Broken CLI group.""" + pass + + +def test_registered(): + # Make sure the plugins are properly registered. If this test fails it + # means that some of the for loops in other tests may not be executing. + assert len([ep for ep in iter_entry_points('_test_click_plugins.test_plugins')]) > 1 + assert len([ep for ep in iter_entry_points('_test_click_plugins.broken_plugins')]) > 1 + + +def test_register_and_run(runner): + + result = runner.invoke(good_cli) + assert result.exit_code is 0 + + for ep in iter_entry_points('_test_click_plugins.test_plugins'): + cmd_result = runner.invoke(good_cli, [ep.name, 'something']) + assert cmd_result.exit_code is 0 + assert cmd_result.output.strip() == 'passed' + + +def test_broken_register_and_run(runner): + + result = runner.invoke(broken_cli) + assert result.exit_code is 0 + assert u'\U0001F4A9' in result.output or u'\u2020' in result.output + + for ep in iter_entry_points('_test_click_plugins.broken_plugins'): + cmd_result = runner.invoke(broken_cli, [ep.name]) + assert cmd_result.exit_code is not 0 + assert 'Traceback' in cmd_result.output + + +def test_group_chain(runner): + + # Attach a sub-group to a CLI and get execute it without arguments to make + # sure both the sub-group and all the parent group's commands are present + @good_cli.group() + def sub_cli(): + """Sub CLI.""" + pass + + result = runner.invoke(good_cli) + assert result.exit_code is 0 + assert sub_cli.name in result.output + for ep in iter_entry_points('_test_click_plugins.test_plugins'): + assert ep.name in result.output + + # Same as above but the sub-group has plugins + @with_plugins(plugins=iter_entry_points('_test_click_plugins.test_plugins')) + @good_cli.group() + def sub_cli_plugins(): + """Sub CLI with plugins.""" + pass + + result = runner.invoke(good_cli, ['sub_cli_plugins']) + assert result.exit_code is 0 + for ep in iter_entry_points('_test_click_plugins.test_plugins'): + assert ep.name in result.output + + # Execute one of the sub-group's commands + result = runner.invoke(good_cli, ['sub_cli_plugins', 'cmd1', 'something']) + assert result.exit_code is 0 + assert result.output.strip() == 'passed' + + +def test_exception(): + # Decorating something that isn't a click.Group() should fail + with pytest.raises(TypeError): + @with_plugins([]) + @click.command() + def cli(): + """Whatever""" -- Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/python-click-plugins.git _______________________________________________ Pkg-grass-devel mailing list [email protected] http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/pkg-grass-devel

