From fdae6b6f89a8f178ef5935e3f4758cc6279d41ef Mon Sep 17 00:00:00 2001 From: "Gavin M. Roy" Date: Tue, 14 Oct 2014 16:32:37 -0400 Subject: [PATCH 1/5] Work in progress on initial commit - Moved from sprockets package - Added plugin support - Fixed conditional cli switches - Import the module when not using a registered sprockets app - Add a setup.cfg for wheel distribution - Update .travis.yml to include wheel distribution - Initial limited tests --- .travis.yml | 24 +++ LICENSE | 41 ++-- MANIFEST.in | 2 + README.md | 4 - README.rst | 98 +++++++++ nose.cfg | 2 + requirements-2.6.txt | 4 + requirements.txt | 5 + setup.cfg | 2 + setup.py | 49 +++++ sprockets/__init__.py | 1 + sprockets/cli/__init__.py | 407 ++++++++++++++++++++++++++++++++++++++ tests.py | 67 +++++++ 13 files changed, 683 insertions(+), 23 deletions(-) create mode 100644 .travis.yml create mode 100644 MANIFEST.in delete mode 100644 README.md create mode 100644 README.rst create mode 100644 nose.cfg create mode 100644 requirements-2.6.txt create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 sprockets/__init__.py create mode 100644 sprockets/cli/__init__.py create mode 100644 tests.py diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..57e8676 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,24 @@ +language: python +python: +- 2.6 +- 2.7 +- pypy +- 3.2 +- 3.3 +- 3.4 +install: +- if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi +- pip install -r requirements.txt +script: nosetests +after_success: +- coveralls +deploy: + distributions: sdist bdist_wheel + provider: pypi + on: + python: 2.7 + tags: true + all_branches: true + user: sprockets + password: + secure: VjpiEYdgOh+FXy0Xh7hoz15OQaViHo0JwkQAZh41EC5CGtLz4hUT3sUxxR0hsBLdFALv7j8yf68UyzyvKho1Rc1apAydDyzGduFBk9jsc9U3ZDjQ2rF+ROWjdjB0PN+yx1BJEui4YeK5W2hCzP9iPs/KRPuce8+UESTDyfZB3Kc= diff --git a/LICENSE b/LICENSE index 1b94692..1380687 100644 --- a/LICENSE +++ b/LICENSE @@ -1,22 +1,25 @@ -The MIT License (MIT) +Copyright (c) 2014 AWeber Communications +All rights reserved. -Copyright (c) 2014 Sprockets +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + * 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 the name of Sprockets nor the names of its contributors may 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/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..9d5d250 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include LICENSE +include README.rst diff --git a/README.md b/README.md deleted file mode 100644 index bf35faf..0000000 --- a/README.md +++ /dev/null @@ -1,4 +0,0 @@ -sprockets.cli -============= - -Command line tool for starting a Sprockets application diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..c6a479b --- /dev/null +++ b/README.rst @@ -0,0 +1,98 @@ +sprockets.cli +============= +The sprockets CLI interface for running applications. Applications are meant +to be run by a controller that is managed by the sprockets CLI interface. + +The sprockets CLI interface loads controller applications that are registered +using setuptools entry points. + + +|Version| |Downloads| |Status| |Coverage| |License| + +Example CLI Usage +----------------- + + # sprockets --help + + usage: sprockets [-h] [--apps] [--plugins] [-e [PLUGIN]] [-s] [-v] [--version] + CONTROLLER ... [APP] + + Command line tool for starting a Sprockets application + + positional arguments: + CONTROLLER Available sprockets application controllers + http HTTP Application Controller + amqp RabbitMQ Worker Controller + APP Application to run + + optional arguments: + -h, --help show this help message and exit + --apps List installed applications + --plugins List installed plugins + -e [PLUGIN], --enable [PLUGIN] + Enable a plugin + -s, --syslog Log to syslog + -v, --verbose Verbose logging output, use -vv for DEBUG level + logging + --version show program's version number and exit + + Find more Sprockets controllers and plugins at + https://sprockets.readthedocs.org + + + +Controllers +----------- + +Each controller is expected to expose at least a ``main(application, args)`` +method that would be invoked when starting the application. Additional, a +controller can implement a ``add_cli_arguments(parser)`` method that will be +invoked when setting up the command line parameters. This allows controllers +to inject configuration directives into the cli. + +Controller API Summary: + +.. code: python + + module.add_cli_arguments(ArgumentParser) # optional + module.main(app_module, argparse.Namespace) + +Plugins +------- + +Plugins are able to inject themselves at multiple points in the application +lifecycle. Plugins that implement a ``initialization(controller)`` method will +see that method invoked before a controller is started. In addition, if a +``on_startup(controller)`` method is defined, it will be invoked after a +Controller has started a application. Finally if a ``on_shutdown(controller)`` +method is defined, it will be invoked when a controller has shutdown. + +Plugin API Summary: + +.. code: python + + plugin.initialize(controller_module) # optional + plugin.on_start(controller_module) # optional + plugin.on_shutdown(controller_module) # optional + +Applications +------------ + +Applications can be a python package or module and if they are registered +to a specific controller, can be referenced by an alias. Application contracts +vary by controller. + +.. |Version| image:: https://badge.fury.io/py/sprockets.svg? + :target: http://badge.fury.io/py/sprockets + +.. |Status| image:: https://travis-ci.org/sprockets/sprockets.svg?branch=master + :target: https://travis-ci.org/sprockets/sprockets + +.. |Coverage| image:: https://coveralls.io/repos/sprockets/sprockets/badge.png + :target: https://coveralls.io/r/sprockets/sprockets + +.. |Downloads| image:: https://pypip.in/d/sprockets/badge.svg? + :target: https://pypi.python.org/pypi/sprockets + +.. |License| image:: https://pypip.in/license/sprockets/badge.svg? + :target: https://sprockets.readthedocs.org diff --git a/nose.cfg b/nose.cfg new file mode 100644 index 0000000..f8747ca --- /dev/null +++ b/nose.cfg @@ -0,0 +1,2 @@ +[nosetests] +verbosity=3 diff --git a/requirements-2.6.txt b/requirements-2.6.txt new file mode 100644 index 0000000..52981dd --- /dev/null +++ b/requirements-2.6.txt @@ -0,0 +1,4 @@ +argparse +importlib +logutils +unittest2 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c3a3eb9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +tornado>=4.0 +coverage +mock +nose +python-coveralls diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..7c2b287 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fddcda1 --- /dev/null +++ b/setup.py @@ -0,0 +1,49 @@ +from setuptools import setup +import sys + +requirements = [] +tests_require = ['coverage', 'coveralls', 'nose'] + +# Requirements for Python 2.6 +if sys.version_info < (2, 7): + requirements.append('argparse') + requirements.append('importlib') + requirements.append('logutils') + tests_require.append('unittest2') +if sys.version_info < (3, 0): + tests_require.append('mock') + + +setup(name='sprockets.cli', + version='0.1.0', + description='Sprockets command line application runner', + entry_points={'console_scripts': ['sprockets=sprockets.cli:main']}, + author='AWeber Communications', + url='https://github.com/sprockets/sprockets', + install_requires=requirements, + license=open('LICENSE').read(), + namespace_packages=['sprockets'], + package_data={'': ['LICENSE', 'README.rst']}, + packages=['sprockets', 'sprockets.cli'], + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Environment :: No Input/Output (Daemon)', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: Libraries :: Python Modules' + ], + test_suite='nose.collector', + tests_require=tests_require, + zip_safe=False) diff --git a/sprockets/__init__.py b/sprockets/__init__.py new file mode 100644 index 0000000..de40ea7 --- /dev/null +++ b/sprockets/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/sprockets/cli/__init__.py b/sprockets/cli/__init__.py new file mode 100644 index 0000000..ae2b0db --- /dev/null +++ b/sprockets/cli/__init__.py @@ -0,0 +1,407 @@ +""" +Sprockets CLI +============= +The sprockets CLI interface for running applications. Applications are meant +to be run by a controller that is managed by the sprockets CLI interface. + +The sprockets CLI interface loads controller applications that are registered +using setuptools entry points. + +Controllers +----------- + +Each controller is expected to expose at least a ``main(application, args)`` +method that would be invoked when starting the application. Additional, a +controller can implement a ``add_cli_arguments(parser)`` method that will be +invoked when setting up the command line parameters. This allows controllers +to inject configuration directives into the cli. + +Controller API Summary: + +.. code: python + + module.add_cli_arguments(ArgumentParser) # optional + module.main(app_module, argparse.Namespace) + +Plugins +------- + +Plugins are able to inject themselves at multiple points in the application +lifecycle. Plugins that implement a ``initialization(controller)`` method will +see that method invoked before a controller is started. In addition, if a +``on_startup(controller)`` method is defined, it will be invoked after a +Controller has started a application. Finally if a ``on_shutdown(controller)`` +method is defined, it will be invoked when a controller has shutdown. + +Plugin API Summary: + +.. code: python + + plugin.initialize(controller_module) # optional + plugin.on_start(controller_module) # optional + plugin.on_shutdown(controller_module) # optional + +Applications +------------ + +Applications can be a python package or module and if they are registered +to a specific controller, can be referenced by an alias. Application contracts +vary by controller. + +""" +version_info = (0, 1, 0) +__version__ = '.'.join(str(v) for v in version_info) + +import argparse +import importlib +import json +import logging +import string +import sys + +# import logutils for Python 2.6 or logging.config for later versions +if sys.version_info < (2, 7): + import logutils.dictconfig as logging_config +else: + from logging import config as logging_config + +import pkg_resources + +APP_DESC = 'Command line tool for starting a Sprockets application' + +DESCRIPTION = 'Available sprockets application controllers' + +EPILOG = ('Find more Sprockets controllers and plugins at ' + 'https://sprockets.readthedocs.org') + +# Logging formatters +SYSLOG_FORMAT = ('%(levelname)s ' + '%(name)s.%(funcName)s(): %(message)s') + +VERBOSE_FORMAT = ('%(levelname) -10s %(asctime)s %(process)-6d ' + '%(processName) -20s %(name) -20s ' + '%(funcName) -20s L%(lineno)-6d: %(message)s') + +# Base logging configuration +LOGGING = {'disable_existing_loggers': True, + 'filters': {}, + 'formatters': {'syslog': {'format': SYSLOG_FORMAT}, + 'verbose': {'datefmt': '%Y-%m-%d %H:%M:%S', + 'format': VERBOSE_FORMAT}}, + 'handlers': {'console': {'class': 'logging.StreamHandler', + 'formatter': 'verbose'}, + 'syslog': {'class': 'logging.handlers.SysLogHandler', + 'formatter': 'syslog'}}, + 'incremental': False, + 'loggers': {'sprockets': {'handlers': ['console'], + 'level': logging.WARNING, + 'propagate': True}}, + 'root': {'handlers': [], + 'level': logging.CRITICAL, + 'propagate': True}, + 'version': 1} + +LOGGER = logging.getLogger(__name__) + + +class CLI(object): + """The core Sprockets CLI application providing argument parsing and + logic for starting a controller. + + """ + CONTROLLERS = 'sprockets.controller' + PLUGINS = 'sprockets.plugin' + + def __init__(self): + self._controllers = self._get_controllers() + self._plugins = self._get_plugins() + self.arg_parser = argparse.ArgumentParser(description=APP_DESC, + epilog=EPILOG) + self._add_cli_args() + self._args = self.arg_parser.parse_args() + + def run(self): + """Evaluate the command line arguments, performing the appropriate + actions so the application can be started. + + """ + # The apps command prevents any other processing of args + if self._args.apps: + if not self._controllers: + print('ERROR: No application controllers installed\n') + sys.exit(1) + self._print_installed_apps(self._args.controller, + self._args.as_json) + sys.exit(0) + + # The plugins command prevents any other processing of args + if self._args.plugins: + self._print_installed_plugins(self._args.as_json) + sys.exit(0) + + # Make sure the specified controller is valid + if self._args.controller not in self._controllers: + sys.stderr.write('ERROR: Controller "%s" not found\n' % + self._args.controller) + sys.exit(-1) + + # Make sure the specified plugins are valid + for plugin in self._args.enable: + if plugin not in self._plugins: + sys.stderr.write('ERROR: Plugin "%s" not found\n' % plugin) + sys.exit(-1) + + # If app is not specified at this point, raise an error + if not self._args.application: + sys.stderr.write('ERROR: Application not specified\n\n') + self.arg_parser.print_help() + sys.exit(-1) + + # If it's a registered app reference by name, get the module name + app_module = self._get_application_module(self._args.controller, + self._args.application) + + # Configure logging based upon the flags + self._configure_logging(app_module, + self._args.verbose, + self._args.syslog) + + # Shortcut to the controller module + controller = self._controllers[self._args.controller] + + # Try and run plugin initialization + for plugin in self._args.enable: + try: + self._plugins[plugin].initialize(controller) + except AttributeError: + LOGGER.debug('Plugin %s.initialize() undefined', plugin) + + # Try and run the controller + try: + self._controllers[self._args.controller].main(app_module, + self._args) + except TypeError as error: + sys.stderr.write('ERROR: could not start the %s controller for %s' + ': %s\n' % (self._args.controller, + app_module, + str(error))) + sys.exit(-1) + + # Try and run plugin initialization + for plugin in self._args.enable: + try: + self._plugins[plugin].on_shutdown(controller) + except AttributeError: + LOGGER.debug('Plugin %s.on_shutdown() undefined', plugin) + + def _add_cli_args(self): + """Add the cli arguments to the argument parser.""" + self.arg_parser.add_argument('--apps', + action='store_true', + help='List installed applications') + + self.arg_parser.add_argument('--plugins', + action='store_true', + help='List installed plugins') + + self.arg_parser.add_argument('--machine-readable', + action='store_true', + dest='as_json', + help='Output application or plugin list ' + 'as JSON') + + self.arg_parser.add_argument('-e', '--enable', + action='append', + metavar='PLUGIN', + default=[], + nargs="?", + help='Enable a plugin') + + self.arg_parser.add_argument('-s', '--syslog', + action='store_true', + help='Log to syslog') + + self.arg_parser.add_argument('-v', '--verbose', + action='count', + help=('Verbose logging output, use -vv ' + 'for DEBUG level logging')) + + self.arg_parser.add_argument('--version', + action='version', + version='sprockets v%s ' % __version__) + + # Controller sub-parser + if '--plugins' not in sys.argv: + subparsers = self.arg_parser.add_subparsers(dest='controller', + help=DESCRIPTION) + + # Iterate through the controllers and add their cli arguments + for key in self._controllers: + help_text = self._get_controller_help(key) + sub_parser = subparsers.add_parser(key, help=help_text) + try: + self._controllers[key].add_cli_arguments(sub_parser) + except AttributeError: + LOGGER.debug('Controller %s.add_cli_arguments() undefined', + key) + + # The application argument + if '--apps' not in sys.argv: + self.arg_parser.add_argument('application', + metavar='APP', + help='Application to run') + + @staticmethod + def _configure_logging(application, verbosity=0, syslog=False): + """Configure logging for the application, setting the appropriate + verbosity and adding syslog if it's enabled. + + :param str application: The application module/package name + :param int verbosity: 1 == INFO, 2 == DEBUG + :param bool syslog: Enable the syslog handler + + """ + # Create a new copy of the logging config that will be modified + config = dict(LOGGING) + + # Increase the logging verbosity + if verbosity == 1: + config['loggers']['sprockets']['level'] = logging.INFO + elif verbosity == 2: + config['loggers']['sprockets']['level'] = logging.DEBUG + + # Add syslog if it's enabled + if syslog: + config['loggers']['sprockets']['handlers'].append('syslog') + + # Copy the sprockets logger to the application + config['loggers'][application] = dict(config['loggers']['sprockets']) + + # Configure logging + logging_config.dictConfig(config) + + def _get_application_module(self, controller, application): + """Return the module for an application. If it's a entry-point + registered application name, return the module name from the entry + points data. If not, the passed in application name is returned. + + :param str controller: The controller type + :param str application: The application name or module + :rtype: str + + """ + for pkg in self._get_applications(controller): + if pkg.name == application: + return pkg.module_name + return importlib.import_module(application) + + @staticmethod + def _get_applications(controller): + """Return a list of application names for the given controller type + that have registered themselves as sprockets applications. + + :param str controller: The type of controller for the applications + :rtype: list + + """ + group_name = 'sprockets.%s.app' % controller + return pkg_resources.iter_entry_points(group=group_name) + + @staticmethod + def _get_argument_parser(): + """Return an instance of the argument parser. + + :return: argparse.ArgumentParser + + """ + return argparse.ArgumentParser() + + def _get_controllers(self): + """Iterate through the installed controller entry points and import + the modules, returning the dict to be assigned to the CLI._controllers + dict. + + :return: dict + + """ + return self._get_package_resources(self.CONTROLLERS) + + def _get_controller_help(self, controller): + """Return the value of the HELP attribute for a controller that should + describe the functionality of the controller. + + :rtype: str|None + + """ + if hasattr(self._controllers[controller], 'HELP'): + return self._controllers[controller].HELP + return None + + @staticmethod + def _get_package_resources(group): + """Iterate through the installed entry points for the specified group, + importing each package, returning a dict of handles by package name. + + :return: dict + + """ + packages = dict() + for pkg in pkg_resources.iter_entry_points(group=group): + packages[pkg.name] = importlib.import_module(pkg.module_name) + return packages + + def _get_plugins(self): + """Iterate through the installed plugin entry points and import + the modules, returning the dict to be assigned to the CLI._plugins + dict. + + :return: dict + + """ + return self._get_package_resources(self.PLUGINS) + + def _print_installed_apps(self, controller, as_json): + """Print out a list of installed sprockets applications + + :param str controller: The name of the controller to get apps for + :param bool as_json: Output the data as json + + + """ + if as_json: + apps = [app.name for app in self._get_applications(controller)] + print(json.dumps({controller: apps})) + return + print('Installed Sprockets {0} Apps\n'.format(controller.upper())) + print("{0:<25} {1:>25}".format('Name', 'Module')) + print(string.ljust('', 51, '-')) + for app in self._get_applications(controller): + print('{0:<25} {1:>25}'.format(app.name, + '({0})'.format(app.module_name))) + print('') + + def _print_installed_plugins(self, as_json): + """Print out a list of installed plugin packages + + :param bool as_json: Output the data as json + + """ + if as_json: + print(json.dumps({'plugins': [p.name for p in self._plugins]})) + return + if not self._plugins: + print('There are no plugins installed\n') + return + print('Installed Sprockets Plugins\n') + print("{0:<25} {1:>25}".format('Name', 'Module')) + print(string.ljust('', 51, '-')) + for loader in self._plugins: + print('{0:<25} {1:>25}'.format(loader.name, + '({0})'.format(loader.module_name))) + print('') + + +def main(): + """Main application runner""" + cli = CLI() + cli.run() diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..aeae903 --- /dev/null +++ b/tests.py @@ -0,0 +1,67 @@ +""" +Test the Sprockets Command Line Interface + +""" +try: + import unittest2 as unittest +except ImportError: + import unittest + +import mock + +from sprockets import cli + + +class InitializationTests(unittest.TestCase): + + @mock.patch('argparse.ArgumentParser.parse_args') + @mock.patch('pkg_resources.iter_entry_points') + @mock.patch('importlib.import_module') + def setUp(self, import_module, iter_entry_points, parse_args): + self.import_module = import_module + self.iter_entry_points = iter_entry_points + self.parse_args = parse_args + + self.app_points = [mock.Mock(name='test_app', + module_name='mock_app')] + self.ctrl_points = [mock.Mock(name='test_http', + module_name='mock_http')] + self.plugin_points = [mock.Mock(name='test_plugin', + module_name='mock_plugin')] + + self.mock_app = mock.Mock() + self.mock_controller = mock.Mock() + self.mock_controller.add_cli_arguments = self.add_cli_arguments = \ + mock.Mock() + self.mock_plugin = mock.Mock() + + def entry_point_side_effect(*args, **kwargs): + if kwargs.get('group') == 'sprockets.controller': + return self.ctrl_points + elif kwargs.get('group') == 'sprockets.plugin': + return self.plugin_points + elif kwargs.get('group') == 'sprockets.test_http.app': + return self.app_points + self.iter_entry_points.side_effect = entry_point_side_effect + + def import_module_side_effect(*args, **kwargs): + if args[0] == 'mock_app': + return self.mock_app + elif args[0] == 'mock_http': + return self.mock_controller + elif args[0] == 'mock_plugin': + return self.mock_plugin + self.import_module.side_effect = import_module_side_effect + self.obj = cli.CLI() + + def test_pkg_resources_iterated(self): + calls = [mock.call(group='sprockets.controller'), + mock.call(group='sprockets.plugin')] + self.iter_entry_points.assert_has_calls(calls) + + def test_controller_packages_imported(self): + self.import_module.assert_has_calls([mock.call('mock_http'), + mock.call('mock_plugin')]) + + #def test_controller_argparse_method_invoked(self): + # self.add_cli_arguments.assert_called_once_with(self.obj.arg_parser) From 3e9ac83d08539b5b9e98ac2958656d102c4b9055 Mon Sep 17 00:00:00 2001 From: "Gavin M. Roy" Date: Tue, 14 Oct 2014 16:38:59 -0400 Subject: [PATCH 2/5] README formatting updates --- README.rst | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index c6a479b..9a07a2d 100644 --- a/README.rst +++ b/README.rst @@ -12,6 +12,11 @@ using setuptools entry points. Example CLI Usage ----------------- +Help +```` + +.. code:: + # sprockets --help usage: sprockets [-h] [--apps] [--plugins] [-e [PLUGIN]] [-s] [-v] [--version] @@ -39,7 +44,12 @@ Example CLI Usage Find more Sprockets controllers and plugins at https://sprockets.readthedocs.org +Starting a Web App with the NewRelic plugin +``````````````````````````````````````````` +.. code:: + + # sprockets -e newrelic http my_web_app Controllers ----------- @@ -52,7 +62,7 @@ to inject configuration directives into the cli. Controller API Summary: -.. code: python +.. code:: python module.add_cli_arguments(ArgumentParser) # optional module.main(app_module, argparse.Namespace) @@ -69,7 +79,7 @@ method is defined, it will be invoked when a controller has shutdown. Plugin API Summary: -.. code: python +.. code:: python plugin.initialize(controller_module) # optional plugin.on_start(controller_module) # optional @@ -82,17 +92,17 @@ Applications can be a python package or module and if they are registered to a specific controller, can be referenced by an alias. Application contracts vary by controller. -.. |Version| image:: https://badge.fury.io/py/sprockets.svg? - :target: http://badge.fury.io/py/sprockets +.. |Version| image:: https://badge.fury.io/py/sprockets.cli.svg? + :target: http://badge.fury.io/py/sprockets.cli -.. |Status| image:: https://travis-ci.org/sprockets/sprockets.svg?branch=master - :target: https://travis-ci.org/sprockets/sprockets +.. |Status| image:: https://travis-ci.org/sprockets/sprockets.cli.svg?branch=master + :target: https://travis-ci.org/sprockets/sprockets.cli -.. |Coverage| image:: https://coveralls.io/repos/sprockets/sprockets/badge.png - :target: https://coveralls.io/r/sprockets/sprockets +.. |Coverage| image:: https://coveralls.io/repos/sprockets/sprockets.cli/badge.png + :target: https://coveralls.io/r/sprockets/sprockets.cli -.. |Downloads| image:: https://pypip.in/d/sprockets/badge.svg? - :target: https://pypi.python.org/pypi/sprockets +.. |Downloads| image:: https://pypip.in/d/sprockets.cli/badge.svg? + :target: https://pypi.python.org/pypi/sprockets.cli -.. |License| image:: https://pypip.in/license/sprockets/badge.svg? +.. |License| image:: https://pypip.in/license/sprockets.cli/badge.svg? :target: https://sprockets.readthedocs.org From 069800624899b70d98905b7bd4618cda1f1aeaff Mon Sep 17 00:00:00 2001 From: "Gavin M. Roy" Date: Tue, 14 Oct 2014 16:43:01 -0400 Subject: [PATCH 3/5] More README and setup.py updates --- README.rst | 7 ++----- setup.py | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 9a07a2d..542a5ef 100644 --- a/README.rst +++ b/README.rst @@ -6,14 +6,12 @@ to be run by a controller that is managed by the sprockets CLI interface. The sprockets CLI interface loads controller applications that are registered using setuptools entry points. - |Version| |Downloads| |Status| |Coverage| |License| Example CLI Usage ----------------- -Help -```` +Help: .. code:: @@ -44,8 +42,7 @@ Help Find more Sprockets controllers and plugins at https://sprockets.readthedocs.org -Starting a Web App with the NewRelic plugin -``````````````````````````````````````````` +Starting a Web App with the NewRelic plugin: .. code:: diff --git a/setup.py b/setup.py index fddcda1..aad5132 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup(name='sprockets.cli', description='Sprockets command line application runner', entry_points={'console_scripts': ['sprockets=sprockets.cli:main']}, author='AWeber Communications', - url='https://github.com/sprockets/sprockets', + url='https://github.com/sprockets/sprockets.cli', install_requires=requirements, license=open('LICENSE').read(), namespace_packages=['sprockets'], From 46c07bd9684a7eabec8c3ab7a7b54e06380fb3d9 Mon Sep 17 00:00:00 2001 From: "Gavin M. Roy" Date: Tue, 14 Oct 2014 16:44:37 -0400 Subject: [PATCH 4/5] Add Python 2.6 requirements to .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 57e8676..c0a55e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - 3.3 - 3.4 install: -- if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi +- if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install -r requirements-2.6.txt; fi - pip install -r requirements.txt script: nosetests after_success: From 783f914f080dcbd861f3b032946e6499283fa479 Mon Sep 17 00:00:00 2001 From: "Gavin M. Roy" Date: Wed, 15 Oct 2014 18:38:20 -0400 Subject: [PATCH 5/5] Work in progress update - Add stub classes for testing - Change how testing will work - Make the logger used in logging configuration based upon a class level constant/attribute --- sprockets/cli/__init__.py | 10 +++---- sstubs/__init__.py | 1 + sstubs/app.py | 8 ++++++ sstubs/controller.py | 12 +++++++++ sstubs/plugin.py | 21 +++++++++++++++ tests.py | 57 +++++++++++++++++---------------------- 6 files changed, 72 insertions(+), 37 deletions(-) create mode 100644 sstubs/__init__.py create mode 100644 sstubs/app.py create mode 100644 sstubs/controller.py create mode 100644 sstubs/plugin.py diff --git a/sprockets/cli/__init__.py b/sprockets/cli/__init__.py index ae2b0db..f1f95a0 100644 --- a/sprockets/cli/__init__.py +++ b/sprockets/cli/__init__.py @@ -110,6 +110,7 @@ class CLI(object): """ CONTROLLERS = 'sprockets.controller' + LOGGER = 'sprockets' PLUGINS = 'sprockets.plugin' def __init__(self): @@ -251,8 +252,7 @@ class CLI(object): metavar='APP', help='Application to run') - @staticmethod - def _configure_logging(application, verbosity=0, syslog=False): + def _configure_logging(self, application, verbosity=0, syslog=False): """Configure logging for the application, setting the appropriate verbosity and adding syslog if it's enabled. @@ -266,13 +266,13 @@ class CLI(object): # Increase the logging verbosity if verbosity == 1: - config['loggers']['sprockets']['level'] = logging.INFO + config['loggers'][self.LOGGER]['level'] = logging.INFO elif verbosity == 2: - config['loggers']['sprockets']['level'] = logging.DEBUG + config['loggers'][self.LOGGER]['level'] = logging.DEBUG # Add syslog if it's enabled if syslog: - config['loggers']['sprockets']['handlers'].append('syslog') + config['loggers'][self.LOGGER]['handlers'].append('syslog') # Copy the sprockets logger to the application config['loggers'][application] = dict(config['loggers']['sprockets']) diff --git a/sstubs/__init__.py b/sstubs/__init__.py new file mode 100644 index 0000000..af04ef8 --- /dev/null +++ b/sstubs/__init__.py @@ -0,0 +1 @@ +__author__ = 'gavinr' diff --git a/sstubs/app.py b/sstubs/app.py new file mode 100644 index 0000000..2806de4 --- /dev/null +++ b/sstubs/app.py @@ -0,0 +1,8 @@ +""" +Stub Controller for testing + +""" + + +def run(): + pass diff --git a/sstubs/controller.py b/sstubs/controller.py new file mode 100644 index 0000000..2634dd8 --- /dev/null +++ b/sstubs/controller.py @@ -0,0 +1,12 @@ +""" +Stub Controller for testing + +""" + + +def add_cli_arguments(parser): + pass + + +def main(app, args): + pass diff --git a/sstubs/plugin.py b/sstubs/plugin.py new file mode 100644 index 0000000..69888db --- /dev/null +++ b/sstubs/plugin.py @@ -0,0 +1,21 @@ +""" +Stub Plugin Module + +""" +PRIORITY = 50 + + +def add_cli_arguments(parser): + pass + + +def initialize(controller): + pass + + +def on_start(controller): + pass + + +def on_shutdown(controller): + pass diff --git a/tests.py b/tests.py index aeae903..7bab6f5 100644 --- a/tests.py +++ b/tests.py @@ -12,46 +12,34 @@ import mock from sprockets import cli +class Package(object): + + def __init__(self, name, module_name): + self.name = name + self.module_name = module_name + + class InitializationTests(unittest.TestCase): @mock.patch('argparse.ArgumentParser.parse_args') @mock.patch('pkg_resources.iter_entry_points') - @mock.patch('importlib.import_module') - def setUp(self, import_module, iter_entry_points, parse_args): - self.import_module = import_module + def setUp(self, iter_entry_points, parse_args): self.iter_entry_points = iter_entry_points self.parse_args = parse_args - self.app_points = [mock.Mock(name='test_app', - module_name='mock_app')] - self.ctrl_points = [mock.Mock(name='test_http', - module_name='mock_http')] - self.plugin_points = [mock.Mock(name='test_plugin', - module_name='mock_plugin')] - - self.mock_app = mock.Mock() - self.mock_controller = mock.Mock() - self.mock_controller.add_cli_arguments = self.add_cli_arguments = \ - mock.Mock() - self.mock_plugin = mock.Mock() + self.app_points = [Package('tapp', 'sstubs.app')] + self.ctrl_points = [Package('tcontroller', 'sstubs.controller')] + self.plugin_points = [Package('tplugin', 'sstubs.plugin')] def entry_point_side_effect(*args, **kwargs): if kwargs.get('group') == 'sprockets.controller': - return self.ctrl_points + return iter(self.ctrl_points) elif kwargs.get('group') == 'sprockets.plugin': - return self.plugin_points + return iter(self.plugin_points) elif kwargs.get('group') == 'sprockets.test_http.app': - return self.app_points - self.iter_entry_points.side_effect = entry_point_side_effect + return iter(self.app_points) - def import_module_side_effect(*args, **kwargs): - if args[0] == 'mock_app': - return self.mock_app - elif args[0] == 'mock_http': - return self.mock_controller - elif args[0] == 'mock_plugin': - return self.mock_plugin - self.import_module.side_effect = import_module_side_effect + self.iter_entry_points.side_effect = entry_point_side_effect self.obj = cli.CLI() def test_pkg_resources_iterated(self): @@ -59,9 +47,14 @@ class InitializationTests(unittest.TestCase): mock.call(group='sprockets.plugin')] self.iter_entry_points.assert_has_calls(calls) - def test_controller_packages_imported(self): - self.import_module.assert_has_calls([mock.call('mock_http'), - mock.call('mock_plugin')]) + def test_controller_imported(self): + for attr in ['add_cli_arguments', 'main']: + self.assertTrue(hasattr(self.obj._controllers.get('tcontroller'), + attr)) + + def test_plugin_is_imported(self): + for attr in ['initialize', 'on_start', 'on_shutdown']: + self.assertTrue(hasattr(self.obj._plugins.get('tplugin'), attr)) + + - #def test_controller_argparse_method_invoked(self): - # self.add_cli_arguments.assert_called_once_with(self.obj.arg_parser)