pax_global_header00006660000000000000000000000064126005423110014504gustar00rootroot0000000000000052 comment=be19a8862c1dcefc2e5f707d9b0a54b2eb4122b9 click-plugins-1.0.2/000077500000000000000000000000001260054231100142505ustar00rootroot00000000000000click-plugins-1.0.2/.gitignore000066400000000000000000000013221260054231100162360ustar00rootroot00000000000000# 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/ click-plugins-1.0.2/.travis.yml000066400000000000000000000004521260054231100163620ustar00rootroot00000000000000language: python sudo: false cache: directories: - ~/.cache/pip python: - 2.7 - 3.3 - 3.4 - 3.5 - pypy - pypy3 install: - pip install coveralls - pip install -e .\[dev\] script: - py.test tests --cov click_plugins --cov-report term-missing after_success: - coveralls click-plugins-1.0.2/AUTHORS.txt000066400000000000000000000001321260054231100161320ustar00rootroot00000000000000Authors ======= Kevin Wurster Sean Gillies click-plugins-1.0.2/CHANGES.md000066400000000000000000000004371260054231100156460ustar00rootroot00000000000000Changelog ========= 1.0.2 - 2015-09-23 ------------------ - General packaging and Travis-CI improvements. - Don't include tests in MANIFEST.in - #8 1.0.1 - 2015-08-20 ------------------ - Fixed a typo in an error message - #5 1.0 - 2015-07-20 ---------------- - Initial release. click-plugins-1.0.2/LICENSE.txt000066400000000000000000000027501260054231100160770ustar00rootroot00000000000000New 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. * Neither click-plugins 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.click-plugins-1.0.2/MANIFEST.in000066400000000000000000000001631260054231100160060ustar00rootroot00000000000000include AUTHORS.txt include CHANGES.md include LICENSE.txt include MANIFEST.in include README.rst include setup.py click-plugins-1.0.2/README.rst000066400000000000000000000126621260054231100157460ustar00rootroot00000000000000============= 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 `_ 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 `_ 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 `_ 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 `` --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`` click-plugins-1.0.2/click_plugins/000077500000000000000000000000001260054231100170765ustar00rootroot00000000000000click-plugins-1.0.2/click_plugins/__init__.py000066400000000000000000000043021260054231100212060ustar00rootroot00000000000000""" 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 click_plugins.core import with_plugins __version__ = '1.0.2' __author__ = 'Kevin Wurster, Sean Gillies' __email__ = 'wursterk@gmail.com, sean.gillies@gmail.com' __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. * Neither click-plugins 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. ''' click-plugins-1.0.2/click_plugins/core.py000066400000000000000000000045611260054231100204060ustar00rootroot00000000000000""" 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 attached 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) click-plugins-1.0.2/example/000077500000000000000000000000001260054231100157035ustar00rootroot00000000000000click-plugins-1.0.2/example/PrintIt/000077500000000000000000000000001260054231100172745ustar00rootroot00000000000000click-plugins-1.0.2/example/PrintIt/README.rst000066400000000000000000000001711260054231100207620ustar00rootroot00000000000000PrintIt ======= This represents a core package with a CLI that registers external plugins. All it does is print stuff. click-plugins-1.0.2/example/PrintIt/printit/000077500000000000000000000000001260054231100207655ustar00rootroot00000000000000click-plugins-1.0.2/example/PrintIt/printit/__init__.py000066400000000000000000000000421260054231100230720ustar00rootroot00000000000000""" Tools for printing things """ click-plugins-1.0.2/example/PrintIt/printit/cli.py000066400000000000000000000015751260054231100221160ustar00rootroot00000000000000""" 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')) @click.group() def cli(): """ Format and print file contents. \b For example: \b $ cat README.rst | printit lower """ @cli.command() @click.argument('infile', type=click.File('r'), default='-') @click.argument('outfile', type=click.File('w'), default='-') def upper(infile, outfile): """ Convert to upper case. """ for line in infile: outfile.write(line.upper()) @cli.command() @click.argument('infile', type=click.File('r'), default='-') @click.argument('outfile', type=click.File('w'), default='-') def lower(infile, outfile): """ Convert to lower case. """ for line in infile: outfile.write(line.lower()) click-plugins-1.0.2/example/PrintIt/printit/core.py000066400000000000000000000000571260054231100222710ustar00rootroot00000000000000""" Some other file that does other stuff. """ click-plugins-1.0.2/example/PrintIt/setup.py000077500000000000000000000004011260054231100210040ustar00rootroot00000000000000#!/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 ''' ) click-plugins-1.0.2/example/PrintItBold/000077500000000000000000000000001260054231100200755ustar00rootroot00000000000000click-plugins-1.0.2/example/PrintItBold/README.rst000066400000000000000000000002661260054231100215700ustar00rootroot00000000000000PrintItBold =========== 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. click-plugins-1.0.2/example/PrintItBold/printit_bold/000077500000000000000000000000001260054231100225665ustar00rootroot00000000000000click-plugins-1.0.2/example/PrintItBold/printit_bold/__init__.py000066400000000000000000000000701260054231100246740ustar00rootroot00000000000000""" A CLI plugin for `PrintIt` that adds bold text. """ click-plugins-1.0.2/example/PrintItBold/printit_bold/core.py000066400000000000000000000005171260054231100240730ustar00rootroot00000000000000""" Add bold styling to `printit` """ import click @click.command() @click.argument('infile', type=click.File('r'), default='-') @click.argument('outfile', type=click.File('w'), default='-') def bold(infile, outfile): """ Make text bold. """ for line in infile: click.secho(line, bold=True, file=outfile) click-plugins-1.0.2/example/PrintItBold/setup.py000077500000000000000000000004341260054231100216130ustar00rootroot00000000000000#!/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 ''' ) click-plugins-1.0.2/example/PrintItStyle/000077500000000000000000000000001260054231100203155ustar00rootroot00000000000000click-plugins-1.0.2/example/PrintItStyle/README.rst000066400000000000000000000001311260054231100217770ustar00rootroot00000000000000PrintItStyle ============ A plugin for ``PrintIt`` that adds commands for text styling. click-plugins-1.0.2/example/PrintItStyle/printit_style/000077500000000000000000000000001260054231100232265ustar00rootroot00000000000000click-plugins-1.0.2/example/PrintItStyle/printit_style/__init__.py000066400000000000000000000000761260054231100253420ustar00rootroot00000000000000""" A CLI plugin for `PrintIt` that adds styling options. """ click-plugins-1.0.2/example/PrintItStyle/printit_style/core.py000066400000000000000000000016311260054231100245310ustar00rootroot00000000000000""" Core components for `PrintItStyle` """ import click COLORS = ( 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', ) @click.command() @click.argument('infile', type=click.File('r'), default='-') @click.argument('outfile', type=click.File('w'), default='-') @click.option('-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) @click.command() @click.argument('infile', type=click.File('r'), default='-') @click.argument('outfile', type=click.File('w'), default='-') @click.option('-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) click-plugins-1.0.2/example/PrintItStyle/setup.py000077500000000000000000000005111260054231100220270ustar00rootroot00000000000000#!/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 ''' ) click-plugins-1.0.2/example/README.rst000066400000000000000000000100501260054231100173660ustar00rootroot00000000000000Plugin 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 ''' ) click-plugins-1.0.2/setup.cfg000066400000000000000000000000331260054231100160650ustar00rootroot00000000000000[bdist_wheel] universal = 1click-plugins-1.0.2/setup.py000077500000000000000000000036061260054231100157720ustar00rootroot00000000000000#!/usr/bin/env python """ Setup script for click-plugins """ import codecs import os from setuptools import find_packages from setuptools import setup with codecs.open('README.rst', encoding='utf-8') 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', 'wheel', 'coveralls' ], }, 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(exclude=['tests']), url=source, version=version, zip_safe=True ) click-plugins-1.0.2/tests/000077500000000000000000000000001260054231100154125ustar00rootroot00000000000000click-plugins-1.0.2/tests/__init__.py000066400000000000000000000000721260054231100175220ustar00rootroot00000000000000# This file is required for some of the tests of Python 2 click-plugins-1.0.2/tests/conftest.py000066400000000000000000000002031260054231100176040ustar00rootroot00000000000000from click.testing import CliRunner import pytest @pytest.fixture(scope='function') def runner(request): return CliRunner() click-plugins-1.0.2/tests/test_plugins.py000066400000000000000000000110241260054231100205020ustar00rootroot00000000000000from 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 @click.command() @click.argument('arg') def cmd1(arg): """Test command 1""" click.echo('passed') @click.command() @click.argument('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')) @click.group() def good_cli(): """Good CLI group.""" pass @with_plugins(iter_entry_points('_test_click_plugins.broken_plugins')) @click.group() 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"""