Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from dave-shawley/add-setuptools-ext
Add helper_run setuptools extension.
- Loading branch information
Showing
5 changed files
with
213 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,3 +7,4 @@ helper API | |
controller | ||
logging | ||
parser | ||
setupext |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
Setup Tools Integration | ||
======================= | ||
Helper installs an additional distutils command named *run_helper* that will run a :class:`Controller` directly from your *setup.py*. This is a nice alternative to writing your our shell wrapper for use during development. If *setup.py* is executable, then you can run ``myapp.Controller`` with:: | ||
|
||
./setup.py run_helper -c etc/myapp.yml -C myapp.Controller | ||
|
||
This functionality is a standard *distutils* entry point so it follows all of the same rules as other extensions such as *build_sphinx* or *nosetests*. The command line arguments can be included in the ``[run_helper]`` section of *setup.cfg*:: | ||
|
||
[run_helper] | ||
configuration = etc/myapp.yml | ||
controller = myapp.Controller | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
"""Add a setuptools command that runs a helper-based application.""" | ||
try: | ||
from setuptools import Command | ||
except ImportError: | ||
from distutils.core import Command | ||
|
||
from . import parser | ||
from . import platform | ||
|
||
|
||
class RunCommand(Command): | ||
|
||
"""Run a helper-based application. | ||
This extension is installed as a ``distutils.commands`` | ||
entry point that provides the *run_helper* command. When | ||
run, it imports a :class:`helper.Controller` subclass by | ||
name, creates a new instance, and runs it in the foreground | ||
until interrupted. The dotted-name of the controller class | ||
and an optional configuration file are provided as command | ||
line parameters. | ||
:param str configuration: the name of a configuration file | ||
to pass to the application *(optional)* | ||
:param str controller: the dotted-name of the Python class | ||
to load and run | ||
""" | ||
|
||
description = 'run a helper.Controller' | ||
user_options = [ | ||
('configuration=', 'c', 'path to application configuration file'), | ||
('controller=', 'C', 'controller to run'), | ||
] | ||
|
||
def initialize_options(self): | ||
"""Initialize parameters.""" | ||
self.configuration = None | ||
self.controller = None | ||
|
||
def finalize_options(self): | ||
"""Required override that does nothing.""" | ||
pass | ||
|
||
def run(self): | ||
"""Import the controller and run it. | ||
This mimics the processing done by :func:`helper.start` | ||
when a controller is run in the foreground. A new instance | ||
of ``self.controller`` is created and run until a keyboard | ||
interrupt occurs or the controller stops on its own accord. | ||
""" | ||
segments = self.controller.split('.') | ||
controller_class = reduce(getattr, segments[1:], | ||
__import__('.'.join(segments[:-1]))) | ||
cmd_line = ['-f'] | ||
if self.configuration is not None: | ||
cmd_line.extend(['-c', self.configuration]) | ||
args = parser.get().parse_args(cmd_line) | ||
controller_instance = controller_class(args, platform) | ||
try: | ||
controller_instance.start() | ||
except KeyboardInterrupt: | ||
controller_instance.stop() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
try: | ||
import unittest2 as unittest | ||
except ImportError: | ||
import unittest | ||
|
||
import mock | ||
|
||
import helper.setupext | ||
|
||
|
||
class PatchedTestMixin(object): | ||
|
||
def setUp(self): | ||
super(PatchedTestMixin, self).setUp() | ||
self._patches = [] | ||
|
||
def tearDown(self): | ||
for patcher in self._patches: | ||
patcher.stop() | ||
self._patches = [] | ||
super(PatchedTestMixin, self).tearDown() | ||
|
||
def add_patch(self, name, **kwargs): | ||
self._patches.append(mock.patch('helper.setupext.' + name, **kwargs)) | ||
return self._patches[-1].start() | ||
|
||
|
||
class RunCommandTests(PatchedTestMixin, unittest.TestCase): | ||
|
||
def assertHasAttribute(self, obj, attribute): | ||
if getattr(obj, attribute, mock.sentinel.default) \ | ||
is mock.sentinel.default: | ||
self.fail('Expected {0} to have attribute named {1}'.format( | ||
obj, attribute)) | ||
|
||
def setUp(self): | ||
super(RunCommandTests, self).setUp() | ||
self.add_patch('Command.__init__', return_value=None) | ||
self.command = helper.setupext.RunCommand(mock.Mock()) | ||
|
||
def test_description(self): | ||
self.assertHasAttribute(helper.setupext.RunCommand, 'description') | ||
|
||
def test_user_options(self): | ||
self.assertEquals(helper.setupext.RunCommand.user_options[0], | ||
('configuration=', 'c', mock.ANY)) | ||
self.assertEquals(helper.setupext.RunCommand.user_options[1], | ||
('controller=', 'C', mock.ANY)) | ||
|
||
def test_initialize_options(self): | ||
self.command.initialize_options() | ||
self.assertIsNone(getattr(self.command, 'controller')) | ||
self.assertIsNone(getattr(self.command, 'configuration')) | ||
|
||
def test_finalize_options(self): | ||
finalize_options = self.add_patch('Command.finalize_options') | ||
|
||
self.command.finalize_options() | ||
self.assertFalse(finalize_options.called) | ||
|
||
|
||
|
||
class RunCommandRunTests(PatchedTestMixin, unittest.TestCase): | ||
|
||
def setUp(self): | ||
super(RunCommandRunTests, self).setUp() | ||
self.controller_module = mock.Mock() | ||
self.command_class = mock.Mock() | ||
# `start` runs until Ctrl+C is pressed, mimic that | ||
self.command_class.return_value.start.side_effect = KeyboardInterrupt | ||
|
||
self.add_patch('Command.__init__', return_value=None) | ||
self.import_stmt = self.add_patch('__import__', create=True) | ||
self.getattr_stmt = self.add_patch('getattr', create=True) | ||
self.getattr_stmt.side_effect = [self.controller_module, | ||
self.command_class] | ||
self.platform = self.add_patch('platform') | ||
self.parser = self.add_patch('parser') | ||
|
||
self.command = helper.setupext.RunCommand(mock.Mock()) | ||
self.command.controller = 'package.controller.command' | ||
self.command.configuration = None | ||
|
||
self.command.run() | ||
|
||
def test_controller_is_imported(self): | ||
self.import_stmt.assert_called_once_with('package.controller') | ||
self.getattr_stmt.assert_any_call(self.import_stmt.return_value, | ||
'controller') | ||
self.getattr_stmt.assert_any_call(self.controller_module, 'command') | ||
|
||
def test_parser_is_fetched_from_config(self): | ||
self.parser.get.assert_called_once_with() | ||
|
||
def test_command_line_is_parsed(self): | ||
parser = self.parser.get.return_value | ||
parser.parse_args.assert_called_once_with(['-f']) | ||
|
||
def test_controller_is_created(self): | ||
self.command_class.assert_called_once_with( | ||
self.parser.get.return_value.parse_args.return_value, | ||
self.platform, | ||
) | ||
|
||
def test_should_start_controller(self): | ||
self.command_class.return_value.start.assert_called_once_with() | ||
|
||
def test_should_stop_controller(self): | ||
self.command_class.return_value.stop.assert_called_once_with() | ||
|
||
|
||
class RunCommandParameterTests(PatchedTestMixin, unittest.TestCase): | ||
|
||
def setUp(self): | ||
super(RunCommandParameterTests, self).setUp() | ||
self.add_patch('Command.__init__', return_value=None) | ||
self.add_patch('__import__', create=True) | ||
self.add_patch('getattr', create=True) | ||
|
||
self.parser = self.add_patch('parser') | ||
self.platform = self.add_patch('platform') | ||
|
||
self.command = helper.setupext.RunCommand(mock.Mock()) | ||
self.command.controller = 'some.string' | ||
self.command.configuration = mock.sentinel.config_file_path | ||
self.command.run() | ||
|
||
def test_configuration_parameter_passed_to_parser(self): | ||
parser = self.parser.get.return_value | ||
parser.parse_args.assert_called_once_with( | ||
['-f', '-c', mock.sentinel.config_file_path]) |