[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