Repository: repobee/repobee

Issue:
APIError should be moved to repobee_plug
The `APIError`, and all of its descendant errors, should be moved to repobee_plug, as APIs are plugins nowadays. As `repobee_plug` is now in this repository, this should be fairly easy.

Currently, `APIError` is located in `_repobee.exception`, and currently extends the `RepoBeeException` exception. It should be moved to `repobee_plug._exceptions` and instead extend `repobee_plug._exceptions.PlugError` `APIError` should then be exposed in `repobee_plug`'s top level (i.e. in `src/repobee_plug/__init__.py`).

The intended use would then be:

```python
import repobee_plug as plug

raise plug.APIError("msg", status=404)
```

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/repobee/repobee/pull/538?src=pr&el=h1) Report
> Merging [#538](https://codecov.io/gh/repobee/repobee/pull/538?src=pr&el=desc) into [master](https://codecov.io/gh/repobee/repobee/commit/2d8eecab831a65edf4f838e8ee47a8b687b24406&el=desc) will **decrease** coverage by `0.06%`.
> The diff coverage is `77.14%`.

[![Impacted file tree graph](https://codecov.io/gh/repobee/repobee/pull/538/graphs/tree.svg?width=650&height=150&src=pr&token=5vOKgkphZe)](https://codecov.io/gh/repobee/repobee/pull/538?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #538      +/-   ##
==========================================
- Coverage   88.24%   88.18%   -0.07%     
==========================================
  Files          47       47              
  Lines        2476     2462      -14     
  Branches      406      405       -1     
==========================================
- Hits         2185     2171      -14     
  Misses        241      241              
  Partials       50       50              
```


| [Impacted Files](https://codecov.io/gh/repobee/repobee/pull/538?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/\_repobee/exception.py](https://codecov.io/gh/repobee/repobee/pull/538/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2V4Y2VwdGlvbi5weQ==) | `100.00% <ø> (ø)` | |
| [src/repobee\_plug/\_apimeta.py](https://codecov.io/gh/repobee/repobee/pull/538/diff?src=pr&el=tree#diff-c3JjL3JlcG9iZWVfcGx1Zy9fYXBpbWV0YS5weQ==) | `97.16% <ø> (ø)` | |
| [src/\_repobee/ext/gitlab.py](https://codecov.io/gh/repobee/repobee/pull/538/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2V4dC9naXRsYWIucHk=) | `63.86% <54.54%> (-0.40%)` | :arrow_down: |
| [src/\_repobee/ext/defaults/github.py](https://codecov.io/gh/repobee/repobee/pull/538/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2V4dC9kZWZhdWx0cy9naXRodWIucHk=) | `90.26% <76.92%> (-0.15%)` | :arrow_down: |
| [src/\_repobee/cli/parsing.py](https://codecov.io/gh/repobee/repobee/pull/538/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2NsaS9wYXJzaW5nLnB5) | `86.57% <100.00%> (ø)` | |
| [src/repobee\_plug/\_\_init\_\_.py](https://codecov.io/gh/repobee/repobee/pull/538/diff?src=pr&el=tree#diff-c3JjL3JlcG9iZWVfcGx1Zy9fX2luaXRfXy5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/repobee\_plug/\_exceptions.py](https://codecov.io/gh/repobee/repobee/pull/538/diff?src=pr&el=tree#diff-c3JjL3JlcG9iZWVfcGx1Zy9fZXhjZXB0aW9ucy5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/repobee\_plug/cli/\_corecommand.py](https://codecov.io/gh/repobee/repobee/pull/538/diff?src=pr&el=tree#diff-c3JjL3JlcG9iZWVfcGx1Zy9jbGkvX2NvcmVjb21tYW5kLnB5) | `92.30% <0.00%> (-0.20%)` | :arrow_down: |
| [src/\_repobee/main.py](https://codecov.io/gh/repobee/repobee/pull/538/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL21haW4ucHk=) | `90.69% <0.00%> (-0.11%)` | :arrow_down: |
| ... and [5 more](https://codecov.io/gh/repobee/repobee/pull/538/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/repobee/repobee/pull/538?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/repobee/repobee/pull/538?src=pr&el=footer). Last update [2d8eeca...1b67332](https://codecov.io/gh/repobee/repobee/pull/538?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

slarse: Failure due to flaky integration tests :(

Patch:
diff --git a/src/_repobee/cli/parsing.py b/src/_repobee/cli/parsing.py
index ee7cddf..8022cc5 100644
--- a/src/_repobee/cli/parsing.py
+++ b/src/_repobee/cli/parsing.py
@@ -251,9 +251,9 @@ def _connect_to_api(
     api_class = plug.manager.hook.get_api_class()
     try:
         return api_class(**kwargs)
-    except exception.NotFoundError:
+    except plug.NotFoundError:
         # more informative message
-        raise exception.NotFoundError(
+        raise plug.NotFoundError(
             "either organization {} could not be found, "
             "or the base url '{}' is incorrect".format(org_name, base_url)
         )
diff --git a/src/_repobee/exception.py b/src/_repobee/exception.py
index f1ed370..0e32040 100644
--- a/src/_repobee/exception.py
+++ b/src/_repobee/exception.py
@@ -36,32 +36,6 @@ class FileError(RepoBeeException):
     """Raise when reading or writing to a file errors out."""
 
 
-class APIError(RepoBeeException):
-    """An exception raised when the API responds with an error code."""
-
-    def __init__(self, msg="", status=None):
-        super().__init__(msg)
-        self.status = status
-
-
-class NotFoundError(APIError):
-    """An exception raised when a platform API fails to find a resource."""
-
-
-class ServiceNotFoundError(APIError):
-    """Raise if the base url can't be located."""
-
-
-class BadCredentials(APIError):
-    """Raise when credentials are rejected."""
-
-
-class UnexpectedException(APIError):
-    """An exception raised when an API request raises an unexpected
-    exception.
-    """
-
-
 class GitError(RepoBeeException):
     """A generic error to raise when a git command exits with a non-zero exit
     status.
diff --git a/src/_repobee/ext/defaults/github.py b/src/_repobee/ext/defaults/github.py
index 3a3b576..53ae769 100644
--- a/src/_repobee/ext/defaults/github.py
+++ b/src/_repobee/ext/defaults/github.py
@@ -22,8 +22,6 @@ import github
 
 import repobee_plug as plug
 
-from _repobee import exception
-
 REQUIRED_TOKEN_SCOPES = {"admin:org", "repo"}
 ISSUE_GENERATOR = Generator[plug.Issue, None, None]
 
@@ -54,15 +52,15 @@ DEFAULT_REVIEW_ISSUE = plug.Issue(
 @contextlib.contextmanager
 def _convert_404_to_not_found_error(msg):
     """Catch a github.GithubException with status 404 and convert to
-    exception.NotFoundError with the provided message. If the GithubException
-    does not have status 404, instead raise exception.UnexpectedException.
+    plug.NotFoundError with the provided message. If the GithubException
+    does not have status 404, instead raise plug.UnexpectedException.
     """
     try:
         yield
     except github.GithubException as exc:
         if exc.status == 404:
-            raise exception.NotFoundError(msg)
-        raise exception.UnexpectedException(
+            raise plug.NotFoundError(msg)
+        raise plug.UnexpectedException(
             "An unexpected exception occured. {.__name__}: {}".format(
                 type(exc), str(exc)
             )
@@ -78,11 +76,11 @@ def _try_api_request(ignore_statuses: Optional[Iterable[int]] = None):
         applicable if the exception is a github.GithubException).
 
     Raises:
-        exception.NotFoundError
-        exception.BadCredentials
-        exception.APIError
-        exception.ServiceNotFoundError
-        exception.UnexpectedException
+        plug.NotFoundError
+        plug.BadCredentials
+        plug.APIError
+        plug.ServiceNotFoundError
+        plug.UnexpectedException
     """
     try:
         yield
@@ -91,20 +89,20 @@ def _try_api_request(ignore_statuses: Optional[Iterable[int]] = None):
             return
 
         if e.status == 404:
-            raise exception.NotFoundError(str(e), status=404)
+            raise plug.NotFoundError(str(e), status=404)
         elif e.status == 401:
-            raise exception.BadCredentials(
+            raise plug.BadCredentials(
                 "credentials rejected, verify that token has correct access.",
                 status=401,
             )
         else:
-            raise exception.APIError(str(e), status=e.status)
+            raise plug.APIError(str(e), status=e.status)
     except gaierror:
-        raise exception.ServiceNotFoundError(
+        raise plug.ServiceNotFoundError(
             "GitHub service could not be found, check the url"
         )
     except Exception as e:
-        raise exception.UnexpectedException(
+        raise plug.UnexpectedException(
             "a {} occured unexpectedly: {}".format(type(e).__name__, str(e))
         )
 
@@ -222,7 +220,7 @@ class GitHubAPI(plug.API):
                 existing_users.append(self._github.get_user(name))
             except github.GithubException as exc:
                 if exc.status != 404:
-                    raise exception.APIError(
+                    raise plug.APIError(
                         "Got unexpected response code from the GitHub API",
                         status=exc.status,
                     )
@@ -268,7 +266,7 @@ class GitHubAPI(plug.API):
             A list of Team namedtuples representing the teams corresponding to
             the provided team_names.
         Raises:
-            exception.UnexpectedException if anything but a 422 (team already
+            plug.UnexpectedException if anything but a 422 (team already
             exists) is raised when trying to create a team.
         """
         existing_teams = list(self._org.get_teams())
@@ -632,7 +630,7 @@ class GitHubAPI(plug.API):
         """See :py:meth:`repobee_plug.API.verify_settings`."""
         LOGGER.info("Verifying settings ...")
         if not token:
-            raise exception.BadCredentials(
+            raise plug.BadCredentials(
                 msg="token is empty. Check that REPOBEE_TOKEN environment "
                 "variable is properly set, or supply the `--token` option."
             )
@@ -656,13 +654,13 @@ class GitHubAPI(plug.API):
                     "{} Possible reasons: bad api url that points to a "
                     "GitHub instance, but not to the api endpoint."
                 ).format(msg)
-                raise exception.UnexpectedException(msg=msg)
+                raise plug.UnexpectedException(msg=msg)
             elif user_.login != user:
                 msg = (
                     "{} Possible reasons: unknown, rerun with -tb and open an "
                     "issue on GitHub.".format(msg)
                 )
-                raise exception.UnexpectedException(msg=msg)
+                raise plug.UnexpectedException(msg=msg)
         LOGGER.info(
             "SUCCESS: found user {}, "
             "user exists and base url looks okay".format(user)
@@ -671,7 +669,7 @@ class GitHubAPI(plug.API):
         LOGGER.info("Verifying access token scopes ...")
         scopes = g.oauth_scopes
         if not REQUIRED_TOKEN_SCOPES.issubset(scopes):
-            raise exception.BadCredentials(
+            raise plug.BadCredentials(
                 "missing one or more access token scopes. "
                 "Actual: {}. Required {}".format(scopes, REQUIRED_TOKEN_SCOPES)
             )
@@ -705,7 +703,7 @@ class GitHubAPI(plug.API):
             owner.login for owner in org.get_members(role="admin")
         )
         if user not in owner_usernames:
-            raise exception.BadCredentials(
+            raise plug.BadCredentials(
                 "user {} is not an owner of organization {}".format(
                     user, org_name
                 )
diff --git a/src/_repobee/ext/gitlab.py b/src/_repobee/ext/gitlab.py
index fa9a41d..76b3530 100644
--- a/src/_repobee/ext/gitlab.py
+++ b/src/_repobee/ext/gitlab.py
@@ -52,8 +52,8 @@ def _convert_404_to_not_found_error(msg):
         yield
     except gitlab.exceptions.GitlabError as exc:
         if exc.response_code == 404:
-            raise exception.NotFoundError(msg)
-        raise exception.UnexpectedException(
+            raise plug.NotFoundError(msg)
+        raise plug.UnexpectedException(
             f"An unexpected exception occured. {type(exc).__name__}: {exc}"
         )
 
@@ -81,18 +81,18 @@ def _try_api_request(ignore_statuses: Optional[Iterable[int]] = None):
             return
 
         if e.response_code == 404:
-            raise exception.NotFoundError(str(e), status=404) from e
+            raise plug.NotFoundError(str(e), status=404) from e
         elif e.response_code == 401:
-            raise exception.BadCredentials(
+            raise plug.BadCredentials(
                 "credentials rejected, verify that token has correct access.",
                 status=401,
             ) from e
         else:
-            raise exception.APIError(str(e), status=e.response_code) from e
+            raise plug.APIError(str(e), status=e.response_code) from e
     except (exception.RepoBeeException, plug.PlugError):
         raise
     except Exception as e:
-        raise exception.UnexpectedException(
+        raise plug.UnexpectedException(
             f"a {type(e).__name__} occured unexpectedly: {str(e)}"
         ) from e
 
@@ -203,7 +203,7 @@ class GitLabAPI(plug.API):
         ]
 
         if not matches:
-            raise exception.NotFoundError(org_name, status=404)
+            raise plug.NotFoundError(org_name, status=404)
 
         return matches[0]
 
@@ -258,7 +258,7 @@ class GitLabAPI(plug.API):
             A list of Team namedtuples representing the teams corresponding to
             the provided team_names.
         Raises:
-            exception.UnexpectedException if anything but a 422 (team already
+            plug.UnexpectedException if anything but a 422 (team already
             exists) is raised when trying to create a team.
         """
         existing_teams = list(self._gitlab.groups.list(id=self._group.id))
@@ -386,7 +386,7 @@ class GitLabAPI(plug.API):
         if missing:
             msg = f"Can't find repos: {', '.join(missing)}"
             if strict:
-                raise exception.NotFoundError(msg)
+                raise plug.NotFoundError(msg)
             LOGGER.warning(msg)
 
     def delete_teams(self, team_names: Iterable[str]) -> None:
@@ -612,7 +612,7 @@ class GitLabAPI(plug.API):
         """See :py:meth:`repobee_plug.API.verify_settings`."""
         LOGGER.info("GitLabAPI is verifying settings ...")
         if not token:
-            raise exception.BadCredentials(
+            raise plug.BadCredentials(
                 msg="Token is empty. Check that REPOBEE_TOKEN environment "
                 "variable is properly set, or supply the `--token` option."
             )
@@ -624,11 +624,11 @@ class GitLabAPI(plug.API):
         LOGGER.info(f"Authenticating connection to {base_url}...")
         with _convert_error(
             gitlab.exceptions.GitlabAuthenticationError,
-            exception.BadCredentials,
+            plug.BadCredentials,
             "Could not authenticate token",
         ), _convert_error(
             requests.exceptions.ConnectionError,
-            exception.APIError,
+            plug.APIError,
             f"Could not connect to {base_url}, please check the URL",
         ):
             gl.auth()
@@ -654,7 +654,7 @@ class GitLabAPI(plug.API):
             if group.path == group_name
         ]
         if not slug_matched:
-            raise exception.NotFoundError(
+            raise plug.NotFoundError(
                 f"Could not find group with slug {group_name}. Verify that "
                 f"you have access to the group, and that you've provided "
                 f"the slug (the name in the address bar)."
@@ -672,7 +672,7 @@ class GitLabAPI(plug.API):
             and member.access_level == gitlab.OWNER_ACCESS
         ]
         if not matching_members:
-            raise exception.BadCredentials(
+            raise plug.BadCredentials(
                 f"User {user} is not an owner of {group_name}"
             )
         LOGGER.info(f"SUCCESS: User {user} is an owner of group {group_name}")
diff --git a/src/repobee_plug/__init__.py b/src/repobee_plug/__init__.py
index f30f81a..5c1cfc6 100644
--- a/src/repobee_plug/__init__.py
+++ b/src/repobee_plug/__init__.py
@@ -46,7 +46,15 @@ from repobee_plug._apimeta import (
 )
 
 # Exceptions
-from repobee_plug._exceptions import HookNameError, PlugError
+from repobee_plug._exceptions import (
+    HookNameError,
+    PlugError,
+    APIError,
+    NotFoundError,
+    ServiceNotFoundError,
+    BadCredentials,
+    UnexpectedException,
+)
 
 manager = pluggy.PluginManager(__package__)
 manager.add_hookspecs(_clone_hook)
@@ -78,6 +86,11 @@ __all__ = [
     # Exceptions
     "HookNameError",
     "PlugError",
+    "APIError",
+    "NotFoundError",
+    "ServiceNotFoundError",
+    "BadCredentials",
+    "UnexpectedException",
     # Helpers
     "json_to_result_mapping",
     "result_mapping_to_json",
diff --git a/src/repobee_plug/_apimeta.py b/src/repobee_plug/_apimeta.py
index e61a94d..2f3431a 100644
--- a/src/repobee_plug/_apimeta.py
+++ b/src/repobee_plug/_apimeta.py
@@ -381,7 +381,7 @@ class APISpec:
         organization member list and checking roles)
 
         Should raise an appropriate subclass of
-        :py:class:`_repobee.exception.APIError` when a problem is encountered.
+        :py:class:`_repobee.plug.APIError` when a problem is encountered.
 
         Args:
             user: The username to try to fetch.
@@ -392,7 +392,7 @@ class APISpec:
         Returns:
             True if the connection is well formed.
         Raises:
-            :py:class:`_repobee.exception.APIError`
+            :py:class:`_repobee.plug.APIError`
         """
         _not_implemented()
 
@@ -482,7 +482,7 @@ class API(APISpec, metaclass=APIMeta):
 
     1. Public methods *must* override one of the public methods of
        :py:class:`APISpec`. If an inheriting class defines any other public
-       method, an :py:class:`~_repobee.exception.APIError` is raised when the
+       method, an :py:class:`~_repobee.plug.APIError` is raised when the
        class is defined.
     2. All public methods in :py:class:`APISpec` have a default implementation
        that simply raise a :py:class:`NotImplementedError`. There is no
diff --git a/src/repobee_plug/_exceptions.py b/src/repobee_plug/_exceptions.py
index 6d65571..26249e6 100644
--- a/src/repobee_plug/_exceptions.py
+++ b/src/repobee_plug/_exceptions.py
@@ -51,3 +51,29 @@ class HookNameError(PlugError):
 
 class APIImplementationError(PlugError):
     """Raise when an API is defined incorrectly."""
+
+
+class APIError(PlugError):
+    """An exception raised when the API responds with an error code."""
+
+    def __init__(self, msg="", status=None):
+        super().__init__(msg)
+        self.status = status
+
+
+class NotFoundError(APIError):
+    """An exception raised when a platform API fails to find a resource."""
+
+
+class ServiceNotFoundError(APIError):
+    """Raise if the base url can't be located."""
+
+
+class BadCredentials(APIError):
+    """Raise when credentials are rejected."""
+
+
+class UnexpectedException(APIError):
+    """An exception raised when an API request raises an unexpected
+    exception.
+    """


Test patch:
diff --git a/tests/unit_tests/repobee/plugin_tests/test_github.py b/tests/unit_tests/repobee/plugin_tests/test_github.py
index a8a6b47..d1005cf 100644
--- a/tests/unit_tests/repobee/plugin_tests/test_github.py
+++ b/tests/unit_tests/repobee/plugin_tests/test_github.py
@@ -8,7 +8,6 @@ import github
 import repobee_plug as plug
 
 import _repobee.ext.defaults.github as github_plugin
-from _repobee import exception
 from _repobee.ext.defaults.github import REQUIRED_TOKEN_SCOPES
 
 import constants
@@ -389,9 +388,9 @@ class TestEnsureTeamsAndMembers:
     @pytest.mark.parametrize(
         "unexpected_exc",
         [
-            exception.APIError("", 404),
-            exception.APIError("", 400),
-            exception.APIError("", 500),
+            plug.APIError("", 404),
+            plug.APIError("", 400),
+            plug.APIError("", 500),
         ],
     )
     def test_raises_on_non_422_exception(
@@ -406,7 +405,7 @@ class TestEnsureTeamsAndMembers:
 
         organization.create_team.side_effect = raise_
 
-        with pytest.raises(exception.UnexpectedException):
+        with pytest.raises(plug.UnexpectedException):
             api.ensure_teams_and_members(team_wrappers)
 
     def test_running_twice_has_no_ill_effects(
@@ -472,7 +471,7 @@ class TestCreateRepos:
         side_effect_github_exception = [unexpected_exception] + side_effect[1:]
 
         create_repo_mock.side_effect = side_effect_github_exception
-        with pytest.raises(exception.APIError):
+        with pytest.raises(plug.APIError):
             api.create_repos(repo_infos)
 
     def test_returns_all_urls(self, mocker, repos, repo_infos, api):
@@ -834,7 +833,7 @@ class TestVerifySettings:
     def test_empty_token_raises_bad_credentials(
         self, happy_github, monkeypatch, api
     ):
-        with pytest.raises(exception.BadCredentials) as exc_info:
+        with pytest.raises(plug.BadCredentials) as exc_info:
             github_plugin.GitHubAPI.verify_settings(
                 USER, ORG_NAME, BASE_URL, ""
             )
@@ -842,7 +841,7 @@ class TestVerifySettings:
         assert "token is empty" in str(exc_info.value)
 
     def test_incorrect_info_raises_not_found_error(self, github_bad_info, api):
-        with pytest.raises(exception.NotFoundError):
+        with pytest.raises(plug.NotFoundError):
             github_plugin.GitHubAPI.verify_settings(
                 USER, ORG_NAME, BASE_URL, TOKEN
             )
@@ -850,14 +849,14 @@ class TestVerifySettings:
     def test_bad_token_scope_raises(self, happy_github, api):
         type(happy_github).oauth_scopes = PropertyMock(return_value=["repo"])
 
-        with pytest.raises(exception.BadCredentials) as exc_info:
+        with pytest.raises(plug.BadCredentials) as exc_info:
             github_plugin.GitHubAPI.verify_settings(
                 USER, ORG_NAME, BASE_URL, TOKEN
             )
         assert "missing one or more access token scopes" in str(exc_info.value)
 
     def test_not_owner_raises(self, happy_github, organization, api):
-        with pytest.raises(exception.BadCredentials) as exc_info:
+        with pytest.raises(plug.BadCredentials) as exc_info:
             github_plugin.GitHubAPI.verify_settings(
                 NOT_OWNER, ORG_NAME, BASE_URL, TOKEN
             )
@@ -870,7 +869,7 @@ class TestVerifySettings:
         self, happy_github, api
     ):
         happy_github.get_user.side_effect = SERVER_ERROR
-        with pytest.raises(exception.UnexpectedException):
+        with pytest.raises(plug.UnexpectedException):
             api.verify_settings(USER, ORG_NAME, BASE_URL, TOKEN)
 
     def test_none_user_raises(self, happy_github, organization, api):
@@ -880,7 +879,7 @@ class TestVerifySettings:
         """
         happy_github.get_user.side_effect = lambda _: User(login=None)
 
-        with pytest.raises(exception.UnexpectedException) as exc_info:
+        with pytest.raises(plug.UnexpectedException) as exc_info:
             github_plugin.GitHubAPI.verify_settings(
                 USER, ORG_NAME, BASE_URL, TOKEN
             )
@@ -904,7 +903,7 @@ class TestVerifySettings:
             "Possible reasons: unknown",
         ]
 
-        with pytest.raises(exception.UnexpectedException) as exc_info:
+        with pytest.raises(plug.UnexpectedException) as exc_info:
             github_plugin.GitHubAPI.verify_settings(
                 USER, ORG_NAME, BASE_URL, TOKEN
             )
diff --git a/tests/unit_tests/repobee/plugin_tests/test_gitlab.py b/tests/unit_tests/repobee/plugin_tests/test_gitlab.py
index e17876c..2396c63 100644
--- a/tests/unit_tests/repobee/plugin_tests/test_gitlab.py
+++ b/tests/unit_tests/repobee/plugin_tests/test_gitlab.py
@@ -6,7 +6,6 @@ import gitlab
 import repobee_plug as plug
 
 import _repobee
-from _repobee import exception
 
 import constants
 
@@ -302,7 +301,7 @@ class TestInit:
     """Tests for the GitLabAPI constructor."""
 
     def test_raises_api_error_when_target_group_cant_be_found(self):
-        with pytest.raises(exception.NotFoundError):
+        with pytest.raises(plug.NotFoundError):
             _repobee.ext.gitlab.GitLabAPI(BASE_URL, TOKEN, "fake-name")
 
 
@@ -312,16 +311,13 @@ class TestEnsureTeamsAndMembers:
         [
             (
                 gitlab.exceptions.GitlabAuthenticationError(response_code=401),
-                exception.BadCredentials,
+                plug.BadCredentials,
             ),
             (
                 gitlab.exceptions.GitlabGetError(response_code=404),
-                exception.NotFoundError,
-            ),
-            (
-                gitlab.exceptions.GitlabError(response_code=500),
-                exception.APIError,
+                plug.NotFoundError,
             ),
+            (gitlab.exceptions.GitlabError(response_code=500), plug.APIError,),
         ],
     )
     def test_converts_gitlab_errors_to_repobee_errors(
@@ -577,13 +573,13 @@ class TestCreateRepos:
 
 class TestVerifySettings:
     def test_raises_if_token_is_empty(self):
-        with pytest.raises(exception.BadCredentials):
+        with pytest.raises(plug.BadCredentials):
             _repobee.ext.gitlab.GitLabAPI.verify_settings(
                 user=None, org_name=TARGET_GROUP, base_url=BASE_URL, token=""
             )
 
     def test_raises_on_failed_connection(self):
-        with pytest.raises(exception.APIError) as exc_info:
+        with pytest.raises(plug.APIError) as exc_info:
             _repobee.ext.gitlab.GitLabAPI.verify_settings(
                 user=None,
                 org_name=TARGET_GROUP,
@@ -594,7 +590,7 @@ class TestVerifySettings:
         assert "please check the URL" in str(exc_info.value)
 
     def test_raises_on_bad_token(self):
-        with pytest.raises(exception.BadCredentials) as exc_info:
+        with pytest.raises(plug.BadCredentials) as exc_info:
             _repobee.ext.gitlab.GitLabAPI.verify_settings(
                 user=None,
                 org_name=TARGET_GROUP,
@@ -606,7 +602,7 @@ class TestVerifySettings:
 
     def test_raises_if_group_cant_be_found(self):
         non_existing_group = "some-garbage-group"
-        with pytest.raises(exception.NotFoundError) as exc_info:
+        with pytest.raises(plug.NotFoundError) as exc_info:
             _repobee.ext.gitlab.GitLabAPI.verify_settings(
                 user=None,
                 org_name=non_existing_group,
@@ -620,7 +616,7 @@ class TestVerifySettings:
 
     def test_raises_if_master_group_cant_be_found(self):
         non_existing_group = "some-garbage-group"
-        with pytest.raises(exception.NotFoundError) as exc_info:
+        with pytest.raises(plug.NotFoundError) as exc_info:
             _repobee.ext.gitlab.GitLabAPI.verify_settings(
                 user=None,
                 org_name=TARGET_GROUP,
diff --git a/tests/unit_tests/repobee/test_cli.py b/tests/unit_tests/repobee/test_cli.py
index 39a842d..2f6b3f7 100644
--- a/tests/unit_tests/repobee/test_cli.py
+++ b/tests/unit_tests/repobee/test_cli.py
@@ -156,7 +156,7 @@ def parsed_args_all_subparsers(request):
         exception.PushFailedError("some message", 128, b"error", "someurl"),
         exception.CloneFailedError("some message", 128, b"error", "someurl"),
         exception.GitError("some message", 128, b"error"),
-        exception.APIError("some message"),
+        plug.APIError("some message"),
     ],
 )
 def command_all_raise_mock(command_mock, api_class_mock, request):
@@ -526,11 +526,11 @@ class TestBaseParsing:
         """
 
         def raise_(*args, **kwargs):
-            raise exception.NotFoundError("Couldn't find the organization.")
+            raise plug.NotFoundError("Couldn't find the organization.")
 
         api_class_mock.side_effect = raise_
 
-        with pytest.raises(exception.NotFoundError) as exc_info:
+        with pytest.raises(plug.NotFoundError) as exc_info:
             _repobee.cli.parsing.handle_args(
                 [
                     *repobee_plug.cli.CoreCommand.repos.setup.as_name_tuple(),
@@ -546,11 +546,11 @@ class TestBaseParsing:
 
     def test_raises_on_bad_credentials(self, api_class_mock, students_file):
         def raise_(*args, **kwargs):
-            raise exception.BadCredentials("bad credentials")
+            raise plug.BadCredentials("bad credentials")
 
         api_class_mock.side_effect = raise_
 
-        with pytest.raises(exception.BadCredentials) as exc_info:
+        with pytest.raises(plug.BadCredentials) as exc_info:
             _repobee.cli.parsing.handle_args(
                 [
                     *repobee_plug.cli.CoreCommand.repos.setup.as_name_tuple(),
@@ -564,13 +564,13 @@ class TestBaseParsing:
 
     def test_raises_on_invalid_base_url(self, api_class_mock, students_file):
         def raise_(*args, **kwargs):
-            raise exception.ServiceNotFoundError(
+            raise plug.ServiceNotFoundError(
                 "GitHub service could not be found, check the url"
             )
 
         api_class_mock.side_effect = raise_
 
-        with pytest.raises(exception.ServiceNotFoundError) as exc_info:
+        with pytest.raises(plug.ServiceNotFoundError) as exc_info:
             _repobee.cli.parsing.handle_args(
                 [
                     *repobee_plug.cli.CoreCommand.repos.setup.as_name_tuple(),