Skip to content

Commit fcf21b0

Browse files
authored
[feat] Add extension command hook (#22)
1 parent 5116dfa commit fcf21b0

File tree

12 files changed

+251
-34
lines changed

12 files changed

+251
-34
lines changed

.travis/install.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/bin/bash
22

33
if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
4-
4+
brew update --force
55
brew upgrade pyenv
66
eval "$(pyenv init -)"
77
pyenv install 3.5.4 --skip-existing

docs/code.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ pluginmeta
1515
.. automodule:: repobee_plug.pluginmeta
1616
:members:
1717

18+
containers
19+
==========
20+
21+
.. automodule:: repobee_plug.containers
22+
:members:
1823

1924
corehooks
2025
=========

repobee_plug/__init__.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,28 @@
22

33
from repobee_plug.__version import __version__
44
from repobee_plug.pluginmeta import Plugin
5-
from repobee_plug.util import hookimpl as repobee_hook
6-
from repobee_plug.util import HookResult
7-
from repobee_plug.util import Status
5+
from repobee_plug.containers import hookimpl as repobee_hook
6+
from repobee_plug.containers import HookResult
7+
from repobee_plug.containers import Status
8+
from repobee_plug.containers import ExtensionParser
9+
from repobee_plug.containers import ExtensionCommand
810
from repobee_plug.corehooks import PeerReviewHook as _peer_hook
911
from repobee_plug.corehooks import APIHook as _api_hook
1012
from repobee_plug.exthooks import CloneHook as _clone_hook
13+
from repobee_plug.exthooks import ExtensionCommandHook as _ext_command_hook
1114

1215
manager = pluggy.PluginManager(__package__)
1316
manager.add_hookspecs(_clone_hook)
1417
manager.add_hookspecs(_peer_hook)
1518
manager.add_hookspecs(_api_hook)
19+
manager.add_hookspecs(_ext_command_hook)
1620

17-
__all__ = ["Plugin", "manager", "repobee_hook", "HookResult", "Status"]
21+
__all__ = [
22+
"Plugin",
23+
"manager",
24+
"repobee_hook",
25+
"HookResult",
26+
"Status",
27+
"ExtensionParser",
28+
"ExtensionCommand",
29+
]

repobee_plug/__version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.6.0"
1+
__version__ = "0.7.0"

repobee_plug/containers.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Container classes and enums.
2+
3+
.. module:: containers
4+
:synopsis: Container classes and enums.
5+
6+
.. moduleauthor:: Simon Larsén
7+
"""
8+
import collections
9+
import enum
10+
import argparse
11+
import pluggy
12+
import typing
13+
14+
from repobee_plug import exception
15+
16+
17+
hookspec = pluggy.HookspecMarker(__package__)
18+
hookimpl = pluggy.HookimplMarker(__package__)
19+
20+
HookResult = collections.namedtuple("HookResult", ("hook", "status", "msg"))
21+
22+
23+
class ExtensionParser(argparse.ArgumentParser):
24+
"""An ArgumentParser specialized for RepoBee extension commands."""
25+
26+
def __init__(self):
27+
super().__init__(add_help=False)
28+
29+
30+
class ExtensionCommand(
31+
collections.namedtuple(
32+
"ExtensionCommand",
33+
("parser", "name", "help", "description", "callback", "requires_api"),
34+
)
35+
):
36+
"""Class defining an extension command for the RepoBee CLI."""
37+
38+
def __new__(
39+
cls,
40+
parser: ExtensionParser,
41+
name: str,
42+
help: str,
43+
description: str,
44+
callback: typing.Callable[[argparse.Namespace, "apimeta.API"], None],
45+
requires_api: bool = False,
46+
):
47+
"""
48+
Args:
49+
parser: The parser to use for the CLI.
50+
name: Name of the command.
51+
help: Text that will be displayed when running ``repobee -h``
52+
description: Text that will be displayed when calling the ``-h``
53+
option for this specific command. Should be elaborate in
54+
describing the usage of the command.
55+
callback: A callback function that is called if this command is
56+
used on the CLI. It is passed the parsed namespace and the
57+
platform API, and is expected to return nothing.
58+
requires_api: If True, the base arguments required for the platform
59+
API are added as options to the extension command, and the
60+
platform API is then passed to the callback function. It is
61+
then important not to have clashing option names. If False, the
62+
base arguments are not added to the CLI, and None is passed in
63+
place of the API.
64+
"""
65+
if not isinstance(parser, ExtensionParser):
66+
raise exception.ExtensionCommandError(
67+
"parser must be a {.__name__}".format(ExtensionParser)
68+
)
69+
if not callable(callback):
70+
raise exception.ExtensionCommandError("callback must be a callable")
71+
return super().__new__(
72+
cls, parser, name, help, description, callback, requires_api
73+
)
74+
75+
def __eq__(self, other):
76+
"""Two ExtensionCommands are equal if they compare equal in all
77+
respects except for the parser, as argpars.ArgumentParser instances do
78+
not implement __eq__.
79+
"""
80+
_, *rest = self
81+
_, *other_rest = other
82+
return rest == other_rest
83+
84+
85+
class Status(enum.Enum):
86+
"""Status codes enum."""
87+
88+
SUCCESS = "success"
89+
WARNING = "warning"
90+
ERROR = "error"

repobee_plug/corehooks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from typing import Union, Optional, Iterable, List, Mapping, Callable, Tuple
1515

16-
from repobee_plug.util import hookspec
16+
from repobee_plug.containers import hookspec
1717

1818

1919
class PeerReviewHook:

repobee_plug/exception.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,9 @@ class HookNameError(PlugError):
1515
"""Raise when a public method in a class that inherits from
1616
:py:class:`~repobee_plug.Plugin` does not have a hook name.
1717
"""
18+
19+
20+
class ExtensionCommandError(PlugError):
21+
"""Raise when an :py:class:~repobee_plug.containers.ExtensionCommand: is
22+
incorrectly defined.
23+
"""

repobee_plug/exthooks.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
import configparser
1616
from typing import Union, Optional
1717

18-
from repobee_plug.util import hookspec
19-
from repobee_plug.util import HookResult
18+
from repobee_plug.containers import hookspec
19+
from repobee_plug.containers import HookResult
20+
from repobee_plug.containers import ExtensionCommand
2021

2122

2223
class CloneHook:
@@ -65,3 +66,59 @@ def config_hook(self, config_parser: configparser.ConfigParser) -> None:
6566
Args:
6667
config: the config parser after config has been read.
6768
"""
69+
70+
71+
class ExtensionCommandHook:
72+
"""Hooks related to extension commands."""
73+
74+
@hookspec
75+
def create_extension_command(self) -> ExtensionCommand:
76+
"""Create an extension command to add to the RepoBee CLI. The command will
77+
be added as one of the top-level subcommands of RepoBee. It should return
78+
an :py:class:`~repobee_plug.containers.ExtensionCommand`.
79+
80+
.. code-block:: python
81+
82+
def command(args: argparse.Namespace, api: apimeta.API)
83+
84+
The ``command`` function will be called if the extension command is used
85+
on the command line.
86+
87+
Note that the :py:class:`~repobee_plug.containers.RepoBeeExtensionParser` class
88+
is just a thin wrapper around :py:class:`argparse.ArgumentParser`, and can
89+
be used in an identical manner. The following is an example definition
90+
of this hook that adds a subcommand called ``example-command``, that can
91+
be called with ``repobee example-command``.
92+
93+
.. code-block:: python
94+
95+
def callback(args: argparse.Namespace, api: apimeta.API) -> None:
96+
LOGGER.info("callback called with: {}, {}".format(args, api))
97+
98+
@plug.repobee_hook
99+
def create_extension_command():
100+
parser = plug.RepoBeeExtensionParser()
101+
parser.add_argument("-b", "--bb", help="A useless argument")
102+
return plug.ExtensionCommand(
103+
parser=parser,
104+
name="example-command",
105+
help="An example command",
106+
description="Description of an example command",
107+
callback=callback,
108+
)
109+
110+
.. important:
111+
112+
The ``-tb|--traceback`` argument is always added to the parser.
113+
Make sure not to add any conflicting arguments.
114+
115+
.. important::
116+
117+
If you need to use the api, you set ``requires_api=True`` in the
118+
``ExtensionCommand``. This will automatically add the options that
119+
the API requires to the CLI options of the subcommand, and
120+
initialize the api and pass it in.
121+
122+
Returns:
123+
A :py:class:`~repobee_plug.containers.ExtensionCommand`.
124+
"""

repobee_plug/pluginmeta.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
from repobee_plug import exception
44
from repobee_plug import corehooks
55
from repobee_plug import exthooks
6-
from repobee_plug import util
6+
from repobee_plug import containers
77

88
_HOOK_METHODS = {
99
key: value
1010
for key, value in [
1111
*exthooks.CloneHook.__dict__.items(),
1212
*corehooks.PeerReviewHook.__dict__.items(),
1313
*corehooks.APIHook.__dict__.items(),
14+
*exthooks.ExtensionCommandHook.__dict__.items(),
1415
]
1516
if callable(value) and not key.startswith("_")
1617
}
@@ -37,7 +38,7 @@ def __new__(cls, name, bases, attrdict):
3738
methods = cls._extract_public_methods(attrdict)
3839
cls._check_names(methods)
3940
hooked_methods = {
40-
name: util.hookimpl(method) for name, method in methods.items()
41+
name: containers.hookimpl(method) for name, method in methods.items()
4142
}
4243
attrdict.update(hooked_methods)
4344

repobee_plug/util.py

Lines changed: 0 additions & 23 deletions
This file was deleted.

0 commit comments

Comments
 (0)