[python-click-plugins] 01/02: Imported Upstream version 1.0
Johan Van de Wauw
johanvdw-guest at moszumanska.debian.org
Thu Aug 6 21:31:22 UTC 2015
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 <johan.vandewauw at gmail.com>
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 <wursterk at gmail.com>
+Sean Gillies <sean.gillies at gmail.com>
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__ = 'wursterk at gmail.com, sean.gillies at 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.
+
+* 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
+
+
+ at with_plugins(iter_entry_points('printit.plugins'))
+ at click.group()
+def cli():
+
+ """
+ Format and print file contents.
+
+ \b
+ For example:
+ \b
+ $ cat README.rst | printit lower
+ """
+
+
+ at cli.command()
+ at click.argument('infile', type=click.File('r'), default='-')
+ at click.argument('outfile', type=click.File('w'), default='-')
+def upper(infile, outfile):
+
+ """
+ Convert to upper case.
+ """
+
+ for line in infile:
+ outfile.write(line.upper())
+
+
+ at cli.command()
+ at click.argument('infile', type=click.File('r'), default='-')
+ at click.argument('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
+
+
+ at click.command()
+ at click.argument('infile', type=click.File('r'), default='-')
+ at 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)
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',
+)
+
+
+ at click.command()
+ at click.argument('infile', type=click.File('r'), default='-')
+ at click.argument('outfile', type=click.File('w'), default='-')
+ at 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)
+
+
+ at click.command()
+ at click.argument('infile', type=click.File('r'), default='-')
+ at click.argument('outfile', type=click.File('w'), default='-')
+ at 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)
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
+
+
+ at pytest.fixture(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
+ at click.command()
+ at click.argument('arg')
+def cmd1(arg):
+ """Test command 1"""
+ click.echo('passed')
+
+ at click.command()
+ at 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
+ at with_plugins(iter_entry_points('_test_click_plugins.test_plugins'))
+ at click.group()
+def good_cli():
+ """Good CLI group."""
+ pass
+
+ at with_plugins(iter_entry_points('_test_click_plugins.broken_plugins'))
+ at 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"""
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/python-click-plugins.git
More information about the Pkg-grass-devel
mailing list