mirror of
https://github.com/JockeTF/fimfarchive.git
synced 2025-02-08 14:56:44 +01:00
Add root application command module
This commit is contained in:
parent
8d0d738fd1
commit
6c0dfd2263
3 changed files with 307 additions and 0 deletions
|
@ -23,8 +23,10 @@ Command module.
|
||||||
|
|
||||||
|
|
||||||
from .base import Command
|
from .base import Command
|
||||||
|
from .root import RootCommand
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Command',
|
'Command',
|
||||||
|
'RootCommand',
|
||||||
)
|
)
|
||||||
|
|
112
fimfarchive/commands/root.py
Normal file
112
fimfarchive/commands/root.py
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
"""
|
||||||
|
Root command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Fimfarchive, preserves stories from Fimfiction.
|
||||||
|
# Copyright (C) 2015 Joakim Soderlund
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Dict, Type
|
||||||
|
|
||||||
|
from .base import Command
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'RootCommand',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RootCommand(Command):
|
||||||
|
"""
|
||||||
|
The main application command.
|
||||||
|
"""
|
||||||
|
commands: Dict[str, Type[Command]] = dict()
|
||||||
|
|
||||||
|
def load(self, command: str) -> Command:
|
||||||
|
"""
|
||||||
|
Instantiates a command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: Name of the command to instantiate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An instance of the specified command.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
KeyError: If the command does not exist.
|
||||||
|
"""
|
||||||
|
return self.commands[command]()
|
||||||
|
|
||||||
|
def doc(self, command: str, adjust=1, indent=2) -> str:
|
||||||
|
"""
|
||||||
|
Generates documentation for a single command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: Name of the command to document.
|
||||||
|
adjust: Number of spaces before the command summary.
|
||||||
|
indent: Number of spaces before the command name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A name and summary for the command.
|
||||||
|
"""
|
||||||
|
cls = self.load(command)
|
||||||
|
doc = getattr(cls, '__doc__', None)
|
||||||
|
|
||||||
|
if doc:
|
||||||
|
doc = str(doc).strip()
|
||||||
|
doc = doc.split('\n', 1)[0]
|
||||||
|
|
||||||
|
description = [
|
||||||
|
indent * ' ',
|
||||||
|
command.ljust(adjust),
|
||||||
|
str(doc),
|
||||||
|
]
|
||||||
|
|
||||||
|
return ''.join(description)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def usage(self) -> str:
|
||||||
|
"""
|
||||||
|
Generates a list of the available commands.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A usage help text for the application.
|
||||||
|
"""
|
||||||
|
text = [
|
||||||
|
'Usage: COMMAND [PARAMETERS]\n\n',
|
||||||
|
'Fimfarchive, ensuring that history is preseved.\n\n',
|
||||||
|
]
|
||||||
|
|
||||||
|
commands = sorted(cmd for cmd in self.commands.keys())
|
||||||
|
adjust = max(len(cmd) for cmd in commands) + 2
|
||||||
|
|
||||||
|
text.append('Commands:\n')
|
||||||
|
for command in commands:
|
||||||
|
line = self.doc(command, adjust)
|
||||||
|
text.extend((line, '\n'))
|
||||||
|
|
||||||
|
return ''.join(text).strip()
|
||||||
|
|
||||||
|
def __call__(self, *args: str) -> int:
|
||||||
|
try:
|
||||||
|
cmd = self.load(args[0])
|
||||||
|
except (IndexError, KeyError):
|
||||||
|
exit(self.usage)
|
||||||
|
else:
|
||||||
|
return cmd(*args[1:])
|
193
tests/commands/test_root.py
Normal file
193
tests/commands/test_root.py
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
"""
|
||||||
|
Root command tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Fimfarchive, preserves stories from Fimfiction.
|
||||||
|
# Copyright (C) 2015 Joakim Soderlund
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from fimfarchive.commands import Command, RootCommand
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def success():
|
||||||
|
"""
|
||||||
|
Returns a command that succeeds.
|
||||||
|
"""
|
||||||
|
class cls(Command):
|
||||||
|
"""
|
||||||
|
Command that returns 0.
|
||||||
|
|
||||||
|
With a truncated documentation line.
|
||||||
|
"""
|
||||||
|
__call__ = MagicMock(return_value=0)
|
||||||
|
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def failure():
|
||||||
|
"""
|
||||||
|
Returns a command that fails.
|
||||||
|
"""
|
||||||
|
class cls(Command):
|
||||||
|
"""
|
||||||
|
Command that returns 1.
|
||||||
|
|
||||||
|
With a truncated documentation line.
|
||||||
|
"""
|
||||||
|
__call__ = MagicMock(return_value=1)
|
||||||
|
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def root(success, failure):
|
||||||
|
"""
|
||||||
|
Returns a root command with custom subcommands.
|
||||||
|
"""
|
||||||
|
class cls(RootCommand):
|
||||||
|
"""
|
||||||
|
Custom root command.
|
||||||
|
"""
|
||||||
|
commands = {
|
||||||
|
'success': type(success),
|
||||||
|
'failure': type(failure),
|
||||||
|
}
|
||||||
|
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def args():
|
||||||
|
"""
|
||||||
|
Returns random command line arguments.
|
||||||
|
"""
|
||||||
|
return [object() for i in range(3)]
|
||||||
|
|
||||||
|
|
||||||
|
class TestRootCommanad():
|
||||||
|
"""
|
||||||
|
Tests the root command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def mock_usage(self, cmd):
|
||||||
|
"""
|
||||||
|
Returns a mocked usage property for the command.
|
||||||
|
"""
|
||||||
|
usage = PropertyMock()
|
||||||
|
type(cmd).usage = usage
|
||||||
|
return usage
|
||||||
|
|
||||||
|
def test_root_usage(self, root):
|
||||||
|
"""
|
||||||
|
Tests usage contains first line of class documentation.
|
||||||
|
"""
|
||||||
|
doc = root.usage
|
||||||
|
assert "success Command that returns 0." in doc
|
||||||
|
assert "failure Command that returns 1." in doc
|
||||||
|
assert "truncated documentation line" not in doc
|
||||||
|
|
||||||
|
def test_root_call_without_args(self, root, success, failure):
|
||||||
|
"""
|
||||||
|
Tests `SystemExit` is raised if called without arguments.
|
||||||
|
"""
|
||||||
|
usage = self.mock_usage(root)
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
root()
|
||||||
|
|
||||||
|
usage.assert_called_once_with()
|
||||||
|
success.__call__.assert_not_called()
|
||||||
|
failure.__call__.assert_not_called()
|
||||||
|
|
||||||
|
def test_root_call_with_args(self, root, success, failure, args):
|
||||||
|
"""
|
||||||
|
Tests `SystemExit` is raised if called with invalid arguments.
|
||||||
|
"""
|
||||||
|
usage = self.mock_usage(root)
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
root(*args)
|
||||||
|
|
||||||
|
usage.assert_called_once_with()
|
||||||
|
success.__call__.assert_not_called()
|
||||||
|
failure.__call__.assert_not_called()
|
||||||
|
|
||||||
|
def test_success_usage(self, root):
|
||||||
|
"""
|
||||||
|
Tests success usage contains first line of class documentation.
|
||||||
|
"""
|
||||||
|
doc = root.doc('success', adjust=9, indent=0)
|
||||||
|
assert "success Command that returns 0." == doc
|
||||||
|
|
||||||
|
def test_success_call_without_args(self, root, success, failure):
|
||||||
|
"""
|
||||||
|
Tests success can be called without arguments.
|
||||||
|
"""
|
||||||
|
usage = self.mock_usage(root)
|
||||||
|
code = root('success')
|
||||||
|
success.__call__.assert_called_once_with()
|
||||||
|
failure.__call__.assert_not_called()
|
||||||
|
usage.assert_not_called()
|
||||||
|
assert code == 0
|
||||||
|
|
||||||
|
def test_success_call_with_args(self, root, success, failure, args):
|
||||||
|
"""
|
||||||
|
Tests success can be called with arguments.
|
||||||
|
"""
|
||||||
|
usage = self.mock_usage(root)
|
||||||
|
code = root('success', *args)
|
||||||
|
success.__call__.assert_called_once_with(*args)
|
||||||
|
failure.__call__.assert_not_called()
|
||||||
|
usage.assert_not_called()
|
||||||
|
assert code == 0
|
||||||
|
|
||||||
|
def test_failure_usage(self, root):
|
||||||
|
"""
|
||||||
|
Tests failure usage contains first line of class documentation.
|
||||||
|
"""
|
||||||
|
doc = root.doc('failure', adjust=9, indent=0)
|
||||||
|
assert "failure Command that returns 1." == doc
|
||||||
|
|
||||||
|
def test_failure_call_without_args(self, root, success, failure):
|
||||||
|
"""
|
||||||
|
Tests failure can be called without arguments.
|
||||||
|
"""
|
||||||
|
usage = self.mock_usage(root)
|
||||||
|
code = root('failure')
|
||||||
|
success.__call__.assert_not_called()
|
||||||
|
failure.__call__.assert_called_once_with()
|
||||||
|
usage.assert_not_called()
|
||||||
|
assert code == 1
|
||||||
|
|
||||||
|
def test_failure_call_with_args(self, root, success, failure, args):
|
||||||
|
"""
|
||||||
|
Tests failure can be called with arguments.
|
||||||
|
"""
|
||||||
|
usage = self.mock_usage(root)
|
||||||
|
code = root('failure', *args)
|
||||||
|
success.__call__.assert_not_called()
|
||||||
|
failure.__call__.assert_called_once_with(*args)
|
||||||
|
usage.assert_not_called()
|
||||||
|
assert code == 1
|
Loading…
Reference in a new issue