Skip to content

Commit 5116dfa

Browse files
authored
[feat] Add API hooks (#20)
1 parent 54feed2 commit 5116dfa

File tree

10 files changed

+87
-38
lines changed

10 files changed

+87
-38
lines changed

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@
6868
# built documents.
6969
#
7070
# The short X.Y version.
71-
version = '0.5'
71+
version = '0.6'
7272
# The full version, including alpha/beta/rc tags.
73-
release = '0.5.1'
73+
release = '0.6.0'
7474

7575
# The language for content autogenerated by Sphinx. Refer to documentation
7676
# for a list of supported languages.

docs/implementing.rst

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ Implementing hooks and writing internal plugins
22
***********************************************
33
Implementing a hook is fairly simple, and works the same way regardless of what
44
type of hook it is (core or extension). If you are working with your own fork
5-
of ``repobee``, all you have to do is write a small module implementing some hooks,
5+
of RepoBee, all you have to do is write a small module implementing some hooks,
66
and drop it into the ``repobee.ext`` sub-package (i.e. the in directory
7-
``repobee/ext`` in the ``repobee`` repo).
7+
``repobee/ext`` in the RepoBee repo).
88

99
There are two ways to implement hooks: as standalone functions or wrapped in a
1010
class. In the following two sections, we'll implement the
@@ -16,7 +16,7 @@ Hook functions in a plugin class
1616
================================
1717
Wrapping hook implementations in a class inheriting from
1818
:py:class:`~repobee_plug.pluginmeta.Plugin` is the recommended way to write
19-
plugins for ``repobee``. The class does some checks to make sure that all
19+
plugins for RepoBee. The class does some checks to make sure that all
2020
public functions have hook function names, which comes in handy if you are
2121
in the habit of misspelling stuff (aren't we all?). Doing it this way,
2222
``exampleplug.py`` would look like this:
@@ -35,12 +35,14 @@ in the habit of misspelling stuff (aren't we all?). Doing it this way,
3535
class ExamplePlugin(plug.Plugin):
3636
"""Example plugin that implements the act_on_cloned_repo hook."""
3737
38-
def act_on_cloned_repo(self,
39-
path: Union[str, pathlib.Path]) -> plug.HookResult:
38+
def act_on_cloned_repo(
39+
self, path: Union[str, pathlib.Path], api,
40+
) -> plug.HookResult:
4041
"""Do something with a cloned repo.
4142
4243
Args:
4344
path: Path to the student repo.
45+
api: A platform API instance.
4446
Returns:
4547
a plug.HookResult specifying the outcome.
4648
"""
@@ -59,8 +61,8 @@ fact, all public methods in a class deriving from
5961
:py:class:`~repobee_plug.pluginmeta.Plugin` must have names of hook functions,
6062
or the class will fail to be created. You can see that the hook returns a
6163
:py:class:`~repobee_plug.util.HookResult`. This is used for reporting the
62-
results in ``repobee``, and is entirely optional (not all hooks support it,
63-
though). Do note that if ``None`` is returned instead, ``repobee`` will not
64+
results in RepoBee, and is entirely optional (not all hooks support it,
65+
though). Do note that if ``None`` is returned instead, RepoBee will not
6466
report anything for the hook. It is recommended that hooks that can return
6567
``HookResult`` do. For a comprehensive example of an internal plugin
6668
implemented with a class, see the built-in `javac plugin`_.

docs/overview.rst

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Plugin system overview
33

44
Conventions
55
===========
6-
For ``repobee`` to discover a plugin and its hooks, the following conventions
6+
For RepoBee to discover a plugin and its hooks, the following conventions
77
need to be adhered to:
88

99
1. The PyPi package should be named ``repobee-<plugin>``, where ``<plugin>``
@@ -16,32 +16,36 @@ need to be adhered to:
1616

1717
For an example plugin that follows these conventions, have a look at
1818
repobee-junit4_. Granted that the plugin follows these conventions and is
19-
installed, it can be loaded like any other ``repobee`` plugin (see `Using
19+
installed, it can be loaded like any other RepoBee plugin (see `Using
2020
Existing Plugins`_).
2121

2222
Hooks
2323
=====
24-
There are two types of hooks in ``repobee``: *core hooks* and *extension
24+
There are two types of hooks in RepoBee: *core hooks* and *extension
2525
hooks*.
2626

2727
Core hooks
2828
----------
29-
Core hooks provide core functionality for ``repobee``, and always have a
29+
Core hooks provide core functionality for RepoBee, and always have a
3030
default implementation in :py:mod:`repobee.ext.defaults`. Providing a
3131
different plugin implementation will override this behavior, thereby
32-
changing some core part of ``repobee``. In general, only one implementation
33-
of a core hook will run per invocation of ``repobee``. All core hooks are
32+
changing some core part of RepoBee. In general, only one implementation
33+
of a core hook will run per invocation of RepoBee. All core hooks are
3434
defined in :py:mod:`repobee_plug.corehooks`.
3535

36+
.. important::
37+
38+
Note that the default implementations in :py:mod:`repobee.ext.defaults` may
39+
simply be *imported* into the module. They are not necessarily defined
40+
there.
41+
3642
Extension hooks
3743
---------------
38-
Extension hooks extend the functionality of ``repobee`` in various ways.
44+
Extension hooks extend the functionality of RepoBee in various ways.
3945
Unlike the core hooks, there are no default implementations of the extension
4046
hooks, and multiple implementations can be run on each invocation of
41-
``repobee``. All extension hooks are defined in
42-
:py:mod:`repobee_plug.exthooks`. repobee-junit4_ consists solely of extension hooks,
43-
and so do all of the `repobee built-ins`_ except for :py:mod:`repobee.ext.defaults`.
47+
RepoBee. All extension hooks are defined in :py:mod:`repobee_plug.exthooks`.
4448

45-
.. _repobee built-ins: https://repobee.readthedocs.io/en/latest/plugins.html#built-in-plugins
49+
.. _repobee built-ins: https://repobee.readthedocs.io/en/stable/plugins.html#built-in-plugins
4650
.. _repobee-junit4: https://github.com/repobee/repobee-junit4
47-
.. _Using Existing Plugins: https://repobee.readthedocs.io/en/latest/plugins.html#using-existing-plugins
51+
.. _Using Existing Plugins: https://repobee.readthedocs.io/en/stable/plugins.html#using-existing-plugins

repobee_plug/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
from repobee_plug.util import HookResult
77
from repobee_plug.util import Status
88
from repobee_plug.corehooks import PeerReviewHook as _peer_hook
9+
from repobee_plug.corehooks import APIHook as _api_hook
910
from repobee_plug.exthooks import CloneHook as _clone_hook
1011

1112
manager = pluggy.PluginManager(__package__)
1213
manager.add_hookspecs(_clone_hook)
1314
manager.add_hookspecs(_peer_hook)
15+
manager.add_hookspecs(_api_hook)
1416

1517
__all__ = ["Plugin", "manager", "repobee_hook", "HookResult", "Status"]

repobee_plug/__version.py

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

repobee_plug/corehooks.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
.. moduleauthor:: Simon Larsén
1212
"""
1313

14-
from typing import Union, Optional, Iterable, List, Mapping, Callable
14+
from typing import Union, Optional, Iterable, List, Mapping, Callable, Tuple
1515

1616
from repobee_plug.util import hookspec
1717

@@ -21,9 +21,11 @@ class PeerReviewHook:
2121

2222
@hookspec(firstresult=True)
2323
def generate_review_allocations(
24-
self, master_repo_name: str, students: Iterable[str],
25-
num_reviews: int,
26-
review_team_name_function: Callable[[str, str], str]
24+
self,
25+
master_repo_name: str,
26+
students: Iterable[str],
27+
num_reviews: int,
28+
review_team_name_function: Callable[[str, str], str],
2729
) -> Mapping[str, List[str]]:
2830
"""Generate a (peer_review_team -> reviewers) mapping for each student
2931
repository (i.e. <student>-<master_repo_name>), where len(reviewers) =
@@ -52,3 +54,25 @@ def generate_review_allocations(
5254
Returns:
5355
a (peer_review_team -> reviewers) mapping for each student repository.
5456
"""
57+
58+
59+
class APIHook:
60+
"""Hooks related to platform APIs."""
61+
62+
@hookspec(firstresult=True)
63+
def get_api_class(self):
64+
"""Return an API platform class. Must be a subclass of apimeta.API.
65+
66+
Returns:
67+
An apimeta.API subclass.
68+
"""
69+
70+
@hookspec(firstresult=True)
71+
def api_init_requires(self) -> Tuple[str]:
72+
"""Return which of the arguments to apimeta.APISpec.__init__ that the
73+
given API requires. For example, the GitHubAPI requires all, but the
74+
GitLabAPI does not require ``user``.
75+
76+
Returns:
77+
Names of the required arguments.
78+
"""

repobee_plug/exthooks.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ class CloneHook:
2323
"""Hook functions related to cloning repos."""
2424

2525
@hookspec
26-
def act_on_cloned_repo(self, path: Union[str, pathlib.Path],
27-
api) -> Optional[HookResult]:
26+
def act_on_cloned_repo(
27+
self, path: Union[str, pathlib.Path], api
28+
) -> Optional[HookResult]:
2829
"""Do something with a cloned repo.
2930
3031
Args:

repobee_plug/pluginmeta.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
key: value
1010
for key, value in [
1111
*exthooks.CloneHook.__dict__.items(),
12-
*corehooks.PeerReviewHook.__dict__.items()
13-
] if callable(value) and not key.startswith('_')
12+
*corehooks.PeerReviewHook.__dict__.items(),
13+
*corehooks.APIHook.__dict__.items(),
14+
]
15+
if callable(value) and not key.startswith("_")
1416
}
1517

1618

@@ -35,8 +37,7 @@ def __new__(cls, name, bases, attrdict):
3537
methods = cls._extract_public_methods(attrdict)
3638
cls._check_names(methods)
3739
hooked_methods = {
38-
name: util.hookimpl(method)
39-
for name, method in methods.items()
40+
name: util.hookimpl(method) for name, method in methods.items()
4041
}
4142
attrdict.update(hooked_methods)
4243

@@ -49,14 +50,16 @@ def _check_names(methods):
4950
if not method_names.issubset(hook_names):
5051
raise exception.HookNameError(
5152
"public method(s) with non-hook name: {}".format(
52-
", ".join(method_names - hook_names)))
53+
", ".join(method_names - hook_names)
54+
)
55+
)
5356

5457
@staticmethod
5558
def _extract_public_methods(attrdict):
5659
return {
5760
key: value
5861
for key, value in attrdict.items()
59-
if callable(value) and not key.startswith('_')
62+
if callable(value) and not key.startswith("_")
6063
}
6164

6265

repobee_plug/util.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
hookspec = pluggy.HookspecMarker(__package__)
1313
hookimpl = pluggy.HookimplMarker(__package__)
1414

15-
HookResult = collections.namedtuple('HookResult', ('hook', 'status', 'msg'))
15+
HookResult = collections.namedtuple("HookResult", ("hook", "status", "msg"))
1616

1717

1818
class Status(enum.Enum):
1919
"""Status codes enum."""
20-
SUCCESS = 'success'
21-
WARNING = 'warning'
22-
ERROR = 'error'
20+
21+
SUCCESS = "success"
22+
WARNING = "warning"
23+
ERROR = "error"

tests/test_pluginmeta.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ def generate_review_allocations(
4747
):
4848
pass
4949

50+
def get_api_class(self):
51+
pass
52+
53+
def api_init_requires(self):
54+
pass
55+
5056
def test_with_private_methods(self):
5157
"""Private methods should be able to have any names."""
5258

@@ -70,6 +76,12 @@ def generate_review_allocations(
7076
):
7177
pass
7278

79+
def get_api_class(self):
80+
pass
81+
82+
def api_init_requires(self):
83+
pass
84+
7385
def _some_method(self, x, y):
7486
return x + y
7587

0 commit comments

Comments
 (0)