codecov-io: # [Codecov](https://codecov.io/gh/repobee/repobee/pull/254?src=pr&el=h1) Report
> Merging [#254](https://codecov.io/gh/repobee/repobee/pull/254?src=pr&el=desc) into [master](https://codecov.io/gh/repobee/repobee/commit/29e37db7098d3abd855506abddcd63e68fd447a4?src=pr&el=desc) will **decrease** coverage by `0.05%`.
> The diff coverage is `100%`.

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

```diff
@@            Coverage Diff             @@
##           master     #254      +/-   ##
==========================================
- Coverage   89.45%   89.39%   -0.06%     
==========================================
  Files          19       19              
  Lines        1365     1377      +12     
  Branches      251      251              
==========================================
+ Hits         1221     1231      +10     
- Misses        123      125       +2     
  Partials       21       21
```


| [Impacted Files](https://codecov.io/gh/repobee/repobee/pull/254?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [repobee/gitlab\_api.py](https://codecov.io/gh/repobee/repobee/pull/254/diff?src=pr&el=tree#diff-cmVwb2JlZS9naXRsYWJfYXBpLnB5) | `64.86% <ø> (ø)` | :arrow_up: |
| [repobee/github\_api.py](https://codecov.io/gh/repobee/repobee/pull/254/diff?src=pr&el=tree#diff-cmVwb2JlZS9naXRodWJfYXBpLnB5) | `92.65% <100%> (+0.12%)` | :arrow_up: |
| [repobee/cli.py](https://codecov.io/gh/repobee/repobee/pull/254/diff?src=pr&el=tree#diff-cmVwb2JlZS9jbGkucHk=) | `94.5% <100%> (-0.79%)` | :arrow_down: |
| [repobee/apimeta.py](https://codecov.io/gh/repobee/repobee/pull/254/diff?src=pr&el=tree#diff-cmVwb2JlZS9hcGltZXRhLnB5) | `93.47% <100%> (+0.62%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/repobee/repobee/pull/254?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/254?src=pr&el=footer). Last update [29e37db...9f84855](https://codecov.io/gh/repobee/repobee/pull/254?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/repobee/apimeta.py b/repobee/apimeta.py
index 5b92c54..2c2ac49 100644
--- a/repobee/apimeta.py
+++ b/repobee/apimeta.py
@@ -19,6 +19,7 @@ NotImplementedError) for any unimplemented API methods.
 .. moduleauthor:: Simon Larsén
 """
 import inspect
+import enum
 import collections
 from typing import List, Iterable, Optional, Generator, Tuple, Mapping
 
@@ -55,6 +56,48 @@ class APIObject:
         return attr
 
 
+class _EnumAuto(collections.namedtuple("_EnumAuto", "value")):
+    """Simplistic emulation of the enum.auto() feature, as it is not available
+    in Python 3.5. Never create an instance of this class manually, always use
+    the :py:func:`build_auto` function.
+    """
+
+
+def _auto_factory_func():
+    """Return a factory function that builds _EnumAuto instances with
+    increasing values.
+    """
+    cur_value = 0
+
+    def _auto():
+        nonlocal cur_value
+        inst = _EnumAuto(value=cur_value)
+        cur_value += 1
+        return inst
+
+    return _auto
+
+
+build_auto = _auto_factory_func()
+
+
+class TeamPermission(enum.Enum):
+    """Enum specifying team permissions on creating teams. On GitHub, for
+    example, this can be e.g. `push` or `pull`.
+    """
+
+    PUSH = build_auto()
+    PULL = build_auto()
+
+
+class IssueState(enum.Enum):
+    """Enum specifying a possible issue state."""
+
+    OPEN = build_auto()
+    CLOSED = build_auto()
+    ALL = build_auto()
+
+
 def _check_name_length(name):
     """Check that a Team/Repository name does not exceed the maximum GitHub
     allows (100 characters)
@@ -147,7 +190,9 @@ class APISpec:
         _not_implemented()
 
     def ensure_teams_and_members(
-        self, teams: Iterable[Team], permission: str
+        self,
+        teams: Iterable[Team],
+        permission: TeamPermission = TeamPermission.PUSH,
     ) -> List[Team]:
         """Ensure that the teams exist, and that their members are added to the
         teams.
@@ -159,9 +204,7 @@ class APISpec:
         Args:
             teams: A list of teams specifying student groups.
             permission: The permission these teams (or members of them) should
-                be given in regards to repositories. For example, on GitHub,
-                the permission should be "push", and on GitLab, it should be
-                developer (30).
+                be given in regards to associated repositories.
         Returns:
             A list of Team API objects of the teams provided to the function,
             both those that were created and those that already existed.
@@ -219,7 +262,7 @@ class APISpec:
     def get_issues(
         self,
         repo_names: Iterable[str],
-        state: str = "open",
+        state: IssueState = IssueState.OPEN,
         title_regex: str = "",
     ) -> Generator[Tuple[str, Generator[Issue, None, None]], None, None]:
         """Get all issues for the repos in repo_names an return a generator
@@ -228,8 +271,7 @@ class APISpec:
 
         Args:
             repo_names: An iterable of repo names.
-            state: Specifying the state of the issue ('open', 'closed' or
-            'all'). Defaults to 'open'.
+            state: Specifies the state of the issue.
             title_regex: If specified, only issues matching this regex are
             returned. Defaults to the empty string (which matches anything).
         Returns:
diff --git a/repobee/cli.py b/repobee/cli.py
index 75a68a2..0d148bd 100644
--- a/repobee/cli.py
+++ b/repobee/cli.py
@@ -498,23 +498,23 @@ def _add_issue_parsers(base_parsers, subparsers):
         help="List open issues (default).",
         action="store_const",
         dest="state",
-        const="open",
+        const=apimeta.IssueState.OPEN,
     )
     state.add_argument(
         "--closed",
         help="List closed issues.",
         action="store_const",
         dest="state",
-        const="closed",
+        const=apimeta.IssueState.CLOSED,
     )
     state.add_argument(
         "--all",
         help="List all issues (open and closed).",
         action="store_const",
         dest="state",
-        const="all",
+        const=apimeta.IssueState.ALL,
     )
-    list_parser.set_defaults(state="open")
+    list_parser.set_defaults(state=apimeta.IssueState.OPEN)
 
 
 class _OrderedFormatter(argparse.HelpFormatter):
diff --git a/repobee/github_api.py b/repobee/github_api.py
index c96557a..c90868e 100644
--- a/repobee/github_api.py
+++ b/repobee/github_api.py
@@ -29,6 +29,17 @@ ISSUE_GENERATOR = Generator[apimeta.Issue, None, None]
 
 LOGGER = daiquiri.getLogger(__file__)
 
+_TEAM_PERMISSION_MAPPING = {
+    apimeta.TeamPermission.PUSH: "push",
+    apimeta.TeamPermission.PULL: "pull",
+}
+_ISSUE_STATE_MAPPING = {
+    apimeta.IssueState.OPEN: "open",
+    apimeta.IssueState.CLOSED: "closed",
+    apimeta.IssueState.ALL: "all",
+}
+
+
 # classes used internally in this module
 _Team = github.Team.Team
 _User = github.NamedUser.NamedUser
@@ -208,13 +219,16 @@ class GitHubAPI(apimeta.API):
         return existing_users
 
     def ensure_teams_and_members(
-        self, teams: Iterable[apimeta.Team], permission: str = "push"
+        self,
+        teams: Iterable[apimeta.Team],
+        permission: apimeta.TeamPermission = apimeta.TeamPermission.PUSH,
     ) -> List[apimeta.Team]:
         """See :py:func:`repobee.apimeta.APISpec.ensure_teams_and_members`."""
+        raw_permission = _TEAM_PERMISSION_MAPPING[permission]
         member_lists = {team.name: team.members for team in teams}
         raw_teams = self._ensure_teams_exist(
             [team_name for team_name in member_lists.keys()],
-            permission=permission,
+            permission=raw_permission,
         )
 
         for team in [team for team in raw_teams if member_lists[team.name]]:
@@ -379,11 +393,12 @@ class GitHubAPI(apimeta.API):
     def get_issues(
         self,
         repo_names: Iterable[str],
-        state: str = "open",
+        state: apimeta.IssueState = apimeta.IssueState.OPEN,
         title_regex: str = "",
     ) -> Generator[Tuple[str, ISSUE_GENERATOR], None, None]:
         """See :py:func:`repobee.apimeta.APISpec.get_issues`."""
         repos = self._get_repos_by_name(repo_names)
+        raw_state = _ISSUE_STATE_MAPPING[state]
 
         with _try_api_request():
             name_issues_pairs = (
@@ -398,7 +413,7 @@ class GitHubAPI(apimeta.API):
                             author=issue.user.login,
                             implementation=issue,
                         )
-                        for issue in repo.get_issues(state=state)
+                        for issue in repo.get_issues(state=raw_state)
                         if re.match(title_regex or "", issue.title)
                     ),
                 )
diff --git a/repobee/gitlab_api.py b/repobee/gitlab_api.py
index 6c9ac1e..7cd827d 100644
--- a/repobee/gitlab_api.py
+++ b/repobee/gitlab_api.py
@@ -109,7 +109,9 @@ class GitLabAPI(apimeta.API):
             )
 
     def ensure_teams_and_members(
-        self, teams: Iterable[apimeta.Team], permission: str = "push"
+        self,
+        teams: Iterable[apimeta.Team],
+        permission: apimeta.TeamPermission = apimeta.TeamPermission.PUSH,
     ) -> List[apimeta.Team]:
         """See :py:func:`repobee.apimeta.APISpec.ensure_teams_and_members`."""
         member_lists = {team.name: team.members for team in teams}

Refactor all apimeta.APISpec configuration arguments to take enums
This is an important step to make supporting both GitLab and GitHub more smooth. Standout things that need refactoring are:

- [x] `ensure_teams_and_members`
    - `permission: str` -> `permission: TeamPermission`
- [x] `get_issues`
    - `state: str` -> `state: IssueState`

There may be a few others.

diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py
index 0fe4ec9..8083086 100644
--- a/tests/unit_tests/test_cli.py
+++ b/tests/unit_tests/test_cli.py
@@ -42,7 +42,7 @@ VALID_PARSED_ARGS = dict(
     issue=ISSUE,
     title_regex="some regex",
     traceback=False,
-    state="open",
+    state=apimeta.IssueState.OPEN,
     show_body=True,
     author=None,
     token=TOKEN,
diff --git a/tests/unit_tests/test_command.py b/tests/unit_tests/test_command.py
index 6334359..71ad428 100644
--- a/tests/unit_tests/test_command.py
+++ b/tests/unit_tests/test_command.py
@@ -5,6 +5,7 @@ from unittest.mock import patch, MagicMock, call, ANY, PropertyMock
 import pytest
 
 import repobee
+import repobee.github_api
 from repobee import command
 from repobee import git
 from repobee import util
@@ -109,18 +110,18 @@ def git_mock(request, mocker):
     return git_mock
 
 
-def _get_issues(repo_names, state="open", title_regex=""):
+def _get_issues(repo_names, state=apimeta.IssueState.OPEN, title_regex=""):
     """Bogus version of GitHubAPI.get_issues"""
     for repo_name in repo_names:
         if repo_name == STUDENT_REPO_NAMES[-2]:
             # repo without issues
             yield repo_name, iter([])
         elif repo_name in STUDENT_REPO_NAMES:
-            if state == "open":
+            if state == apimeta.IssueState.OPEN:
                 issues = iter(OPEN_ISSUES)
-            elif state == "closed":
+            elif state == apimeta.IssueState.CLOSED:
                 issues = iter(CLOSED_ISSUES)
-            elif state == "all":
+            elif state == apimeta.IssueState.ALL:
                 issues = iter(OPEN_ISSUES + CLOSED_ISSUES)
             else:
                 raise ValueError("Unexpected value for 'state': ", state)
@@ -609,7 +610,14 @@ class TestListIssues:
     it is only tested for stability.
     """
 
-    @pytest.mark.parametrize("state", ("open", "closed", "all"))
+    @pytest.mark.parametrize(
+        "state",
+        (
+            apimeta.IssueState.OPEN,
+            apimeta.IssueState.CLOSED,
+            apimeta.IssueState.ALL,
+        ),
+    )
     @pytest.mark.parametrize("regex", ("", r"^.*$"))
     @pytest.mark.parametrize("show_body", (True, False))
     @pytest.mark.parametrize("author", (None, "slarse"))
diff --git a/tests/unit_tests/test_github_api.py b/tests/unit_tests/test_github_api.py
index 5806fe0..ca68248 100644
--- a/tests/unit_tests/test_github_api.py
+++ b/tests/unit_tests/test_github_api.py
@@ -637,7 +637,9 @@ class TestGetIssues:
     def test_get_all_open_issues(self, repos, issues, api):
         repo_names = [repo.name for repo in repos]
 
-        name_issues_pairs = api.get_issues(repo_names, state="open")
+        name_issues_pairs = api.get_issues(
+            repo_names, state=apimeta.IssueState.OPEN
+        )
 
         found_repos = []
         for repo_name, issue_gen in name_issues_pairs:
@@ -652,9 +654,9 @@ class TestGetIssues:
     def test_get_all_closed_issues(self, repos, issues, api):
         repo_names = [repo.name for repo in repos]
 
-        print(api.get_issues(repo_names, state="closed"))
-
-        name_issues_pairs = api.get_issues(repo_names, state="closed")
+        name_issues_pairs = api.get_issues(
+            repo_names, state=apimeta.IssueState.CLOSED
+        )
 
         found_repos = []
         for repo_name, issue_gen in name_issues_pairs:
@@ -673,7 +675,9 @@ class TestGetIssues:
         repo_names = [repo.name for repo in repos] + [non_existing]
         random.shuffle(repo_names)
 
-        name_issues_pairs = api.get_issues(repo_names, state="open")
+        name_issues_pairs = api.get_issues(
+            repo_names, state=apimeta.IssueState.OPEN
+        )
 
         found_repos = []
         for repo_name, issue_gen in name_issues_pairs:
@@ -693,7 +697,7 @@ class TestGetIssues:
         regex = "^{}$".format(sought_issue.title)
 
         name_issues_pairs = api.get_issues(
-            repo_names, state="open", title_regex=regex
+            repo_names, state=apimeta.IssueState.OPEN, title_regex=regex
         )
 
         found_repos = []

pip install -e .[TEST]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-mock codecov
apt-get update apt-get install -y gcc
3.7
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
appdirs==1.4.4
certifi @ file:///croot/certifi_1671487769961/work/certifi
cffi==1.15.1
charset-normalizer==3.4.1
codecov==2.1.13
colored==1.4.4
coverage==7.2.7
cryptography==44.0.2
daiquiri==3.2.1
Deprecated==1.2.18
exceptiongroup==1.2.2
idna==3.10
importlib-metadata==6.7.0
iniconfig==2.0.0
packaging==24.0
pluggy==1.2.0
pycparser==2.21
PyGithub==2.3.0
PyJWT==2.8.0
PyNaCl==1.5.0
pytest==7.4.4
pytest-cov==4.1.0
pytest-mock==3.11.1
python-gitlab==1.8.0
python-json-logger==3.0.1
-e git+https://github.com/repobee/repobee.git@81bfdf7b72aa9443a24426a3a2d5f1108cc9b0ed#egg=repobee
repobee-plug==0.5.1
requests==2.31.0
six==1.17.0
tomli==2.0.1
typing_extensions==4.7.1
urllib3==2.0.7
wrapt==1.16.0
zipp==3.15.0

name: repobee
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - appdirs==1.4.4
      - cffi==1.15.1
      - charset-normalizer==3.4.1
      - codecov==2.1.13
      - colored==1.4.4
      - coverage==7.2.7
      - cryptography==44.0.2
      - daiquiri==3.2.1
      - deprecated==1.2.18
      - exceptiongroup==1.2.2
      - idna==3.10
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - packaging==24.0
      - pluggy==1.2.0
      - pycparser==2.21
      - pygithub==2.3.0
      - pyjwt==2.8.0
      - pynacl==1.5.0
      - pytest==7.4.4
      - pytest-cov==4.1.0
      - pytest-mock==3.11.1
      - python-gitlab==1.8.0
      - python-json-logger==3.0.1
      - repobee==1.5.1
      - repobee-plug==0.5.1
      - requests==2.31.0
      - six==1.17.0
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - wrapt==1.16.0
      - zipp==3.15.0
prefix: /opt/conda/envs/repobee

tests/unit_tests/test_cli.py::TestDispatchCommand::test_raises_on_invalid_subparser_value tests/unit_tests/test_cli.py::TestDispatchCommand::test_no_crash_on_valid_args[setup] tests/unit_tests/test_cli.py::TestDispatchCommand::test_no_crash_on_valid_args[update] tests/unit_tests/test_cli.py::TestDispatchCommand::test_no_crash_on_valid_args[clone] tests/unit_tests/test_cli.py::TestDispatchCommand::test_no_crash_on_valid_args[migrate] tests/unit_tests/test_cli.py::TestDispatchCommand::test_no_crash_on_valid_args[open-issues] tests/unit_tests/test_cli.py::TestDispatchCommand::test_no_crash_on_valid_args[close-issues] tests/unit_tests/test_cli.py::TestDispatchCommand::test_no_crash_on_valid_args[list-issues] tests/unit_tests/test_cli.py::TestDispatchCommand::test_no_crash_on_valid_args[verify-settings] tests/unit_tests/test_cli.py::TestDispatchCommand::test_no_crash_on_valid_args[assign-reviews] tests/unit_tests/test_cli.py::TestDispatchCommand::test_no_crash_on_valid_args[purge-review-teams] tests/unit_tests/test_cli.py::TestDispatchCommand::test_no_crash_on_valid_args[show-config] tests/unit_tests/test_cli.py::TestDispatchCommand::test_no_crash_on_valid_args[check-reviews] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[setup-command_all_raise_mock0] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[setup-command_all_raise_mock1] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[setup-command_all_raise_mock2] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[setup-command_all_raise_mock3] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[update-command_all_raise_mock0] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[update-command_all_raise_mock1] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[update-command_all_raise_mock2] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[update-command_all_raise_mock3] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[clone-command_all_raise_mock0] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[clone-command_all_raise_mock1] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[clone-command_all_raise_mock2] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[clone-command_all_raise_mock3] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[migrate-command_all_raise_mock0] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[migrate-command_all_raise_mock1] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[migrate-command_all_raise_mock2] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[migrate-command_all_raise_mock3] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[open-issues-command_all_raise_mock0] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[open-issues-command_all_raise_mock1] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[open-issues-command_all_raise_mock2] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[open-issues-command_all_raise_mock3] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[close-issues-command_all_raise_mock0] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[close-issues-command_all_raise_mock1] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[close-issues-command_all_raise_mock2] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[close-issues-command_all_raise_mock3] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[list-issues-command_all_raise_mock0] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[list-issues-command_all_raise_mock1] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[list-issues-command_all_raise_mock2] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[list-issues-command_all_raise_mock3] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[verify-settings-command_all_raise_mock0] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[verify-settings-command_all_raise_mock1] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[verify-settings-command_all_raise_mock2] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[verify-settings-command_all_raise_mock3] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[assign-reviews-command_all_raise_mock0] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[assign-reviews-command_all_raise_mock1] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[assign-reviews-command_all_raise_mock2] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[assign-reviews-command_all_raise_mock3] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[purge-review-teams-command_all_raise_mock0] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[purge-review-teams-command_all_raise_mock1] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[purge-review-teams-command_all_raise_mock2] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[purge-review-teams-command_all_raise_mock3] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[show-config-command_all_raise_mock0] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[show-config-command_all_raise_mock1] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[show-config-command_all_raise_mock2] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[show-config-command_all_raise_mock3] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[check-reviews-command_all_raise_mock0] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[check-reviews-command_all_raise_mock1] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[check-reviews-command_all_raise_mock2] tests/unit_tests/test_cli.py::TestDispatchCommand::test_expected_exception_results_in_system_exit[check-reviews-command_all_raise_mock3] tests/unit_tests/test_cli.py::TestDispatchCommand::test_setup_student_repos_called_with_correct_args tests/unit_tests/test_cli.py::TestDispatchCommand::test_update_student_repos_called_with_correct_args tests/unit_tests/test_cli.py::TestDispatchCommand::test_open_issue_called_with_correct_args tests/unit_tests/test_cli.py::TestDispatchCommand::test_close_issue_called_with_correct_args tests/unit_tests/test_cli.py::TestDispatchCommand::test_migrate_repos_called_with_correct_args tests/unit_tests/test_cli.py::TestDispatchCommand::test_clone_repos_called_with_correct_args tests/unit_tests/test_cli.py::TestDispatchCommand::test_verify_settings_called_with_correct_args tests/unit_tests/test_cli.py::TestDispatchCommand::test_verify_settings_called_with_master_org_name tests/unit_tests/test_cli.py::test_help_calls_add_arguments[setup] tests/unit_tests/test_cli.py::test_help_calls_add_arguments[update] tests/unit_tests/test_cli.py::test_help_calls_add_arguments[clone] tests/unit_tests/test_cli.py::test_help_calls_add_arguments[migrate] tests/unit_tests/test_cli.py::test_help_calls_add_arguments[open-issues] tests/unit_tests/test_cli.py::test_help_calls_add_arguments[close-issues] tests/unit_tests/test_cli.py::test_help_calls_add_arguments[list-issues] tests/unit_tests/test_cli.py::test_help_calls_add_arguments[verify-settings] tests/unit_tests/test_cli.py::test_help_calls_add_arguments[assign-reviews] tests/unit_tests/test_cli.py::test_help_calls_add_arguments[purge-review-teams] tests/unit_tests/test_cli.py::test_help_calls_add_arguments[show-config] tests/unit_tests/test_cli.py::test_help_calls_add_arguments[check-reviews] tests/unit_tests/test_cli.py::TestBaseParsing::test_raises_on_invalid_org tests/unit_tests/test_cli.py::TestBaseParsing::test_raises_on_bad_credentials tests/unit_tests/test_cli.py::TestBaseParsing::test_raises_on_invalid_base_url tests/unit_tests/test_cli.py::TestBaseParsing::test_master_org_overrides_target_org_for_master_repos[setup] tests/unit_tests/test_cli.py::TestBaseParsing::test_master_org_overrides_target_org_for_master_repos[update] tests/unit_tests/test_cli.py::TestBaseParsing::test_master_org_name_defaults_to_org_name[setup] tests/unit_tests/test_cli.py::TestBaseParsing::test_master_org_name_defaults_to_org_name[update] tests/unit_tests/test_cli.py::TestBaseParsing::test_token_env_variable_picked_up[setup] tests/unit_tests/test_cli.py::TestBaseParsing::test_token_env_variable_picked_up[update] tests/unit_tests/test_cli.py::TestBaseParsing::test_token_cli_arg_picked_up[setup] tests/unit_tests/test_cli.py::TestBaseParsing::test_token_cli_arg_picked_up[update] tests/unit_tests/test_cli.py::TestBaseParsing::test_raises_on_non_tls_api_url[http://some_enterprise_host/api/v3] tests/unit_tests/test_cli.py::TestBaseParsing::test_raises_on_non_tls_api_url[ftp://some_enterprise_host/api/v3] tests/unit_tests/test_cli.py::TestBaseParsing::test_raises_on_non_tls_api_url[some_enterprise_host/api/v3] tests/unit_tests/test_cli.py::TestStudentParsing::test_raises_if_students_file_is_not_a_file[setup|['-u', tests/unit_tests/test_cli.py::TestStudentParsing::test_raises_if_students_file_is_not_a_file[update|['-u', tests/unit_tests/test_cli.py::TestStudentParsing::test_raises_if_students_file_is_not_a_file[close-issues|['-mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_raises_if_students_file_is_not_a_file[open-issues|['-mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_parser_listing_students[setup|['-u', tests/unit_tests/test_cli.py::TestStudentParsing::test_parser_listing_students[update|['-u', tests/unit_tests/test_cli.py::TestStudentParsing::test_parser_listing_students[close-issues|['-mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_parser_listing_students[open-issues|['-mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_parser_student_file[setup|['-u', tests/unit_tests/test_cli.py::TestStudentParsing::test_parser_student_file[update|['-u', tests/unit_tests/test_cli.py::TestStudentParsing::test_parser_student_file[close-issues|['-mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_parser_student_file[open-issues|['-mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_student_parsers_raise_on_empty_student_file[setup|['-u', tests/unit_tests/test_cli.py::TestStudentParsing::test_student_parsers_raise_on_empty_student_file[update|['-u', tests/unit_tests/test_cli.py::TestStudentParsing::test_student_parsers_raise_on_empty_student_file[close-issues|['-mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_student_parsers_raise_on_empty_student_file[open-issues|['-mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_parsers_raise_if_both_file_and_listing[setup|['-u', tests/unit_tests/test_cli.py::TestStudentParsing::test_parsers_raise_if_both_file_and_listing[update|['-u', tests/unit_tests/test_cli.py::TestStudentParsing::test_parsers_raise_if_both_file_and_listing[close-issues|['-mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_parsers_raise_if_both_file_and_listing[open-issues|['-mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_student_groups_parsed_correcly[setup|['-u', tests/unit_tests/test_cli.py::TestStudentParsing::test_student_groups_parsed_correcly[update|['-u', tests/unit_tests/test_cli.py::TestStudentParsing::test_student_groups_parsed_correcly[close-issues|['-mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_student_groups_parsed_correcly[open-issues|['-mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_raises_if_generated_team_name_too_long[setup|['-u', tests/unit_tests/test_cli.py::TestStudentParsing::test_raises_if_generated_team_name_too_long[update|['-u', tests/unit_tests/test_cli.py::TestStudentParsing::test_raises_if_generated_team_name_too_long[close-issues|['-mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_raises_if_generated_team_name_too_long[open-issues|['-mn', tests/unit_tests/test_cli.py::TestConfig::test_full_config[setup-extra_args0] tests/unit_tests/test_cli.py::TestConfig::test_full_config[update-extra_args1] tests/unit_tests/test_cli.py::TestConfig::test_full_config[open-issues-extra_args2] tests/unit_tests/test_cli.py::TestConfig::test_missing_option_is_required[-g] tests/unit_tests/test_cli.py::TestConfig::test_missing_option_is_required[-u] tests/unit_tests/test_cli.py::TestConfig::test_missing_option_is_required[-sf] tests/unit_tests/test_cli.py::TestConfig::test_missing_option_is_required[-o] tests/unit_tests/test_cli.py::TestConfig::test_missing_option_is_required[-mo] tests/unit_tests/test_cli.py::TestConfig::test_missing_option_can_be_specified[-g] tests/unit_tests/test_cli.py::TestConfig::test_missing_option_can_be_specified[-u] tests/unit_tests/test_cli.py::TestConfig::test_missing_option_can_be_specified[-sf] tests/unit_tests/test_cli.py::TestConfig::test_missing_option_can_be_specified[-o] tests/unit_tests/test_cli.py::TestConfig::test_missing_option_can_be_specified[-mo] tests/unit_tests/test_cli.py::TestSetupAndUpdateParsers::test_happy_path[setup] tests/unit_tests/test_cli.py::TestSetupAndUpdateParsers::test_happy_path[update] tests/unit_tests/test_cli.py::TestSetupAndUpdateParsers::test_finds_local_repo[setup] tests/unit_tests/test_cli.py::TestSetupAndUpdateParsers::test_finds_local_repo[update] tests/unit_tests/test_cli.py::TestMigrateParser::test_happy_path tests/unit_tests/test_cli.py::TestVerifyParser::test_happy_path tests/unit_tests/test_cli.py::TestCloneParser::test_happy_path tests/unit_tests/test_cli.py::TestCloneParser::test_no_other_parser_gets_parse_hook[setup-extra_args0] tests/unit_tests/test_cli.py::TestCloneParser::test_no_other_parser_gets_parse_hook[update-extra_args1] tests/unit_tests/test_cli.py::TestCloneParser::test_no_other_parser_gets_parse_hook[open-issues-extra_args2] tests/unit_tests/test_cli.py::TestCloneParser::test_no_other_parser_gets_parse_hook[close-issues-extra_args3] tests/unit_tests/test_cli.py::TestCloneParser::test_no_other_parser_gets_parse_hook[verify-settings-extra_args4] tests/unit_tests/test_cli.py::TestCloneParser::test_no_other_parser_gets_parse_hook[migrate-extra_args5] tests/unit_tests/test_cli.py::TestShowConfigParser::test_happy_path tests/unit_tests/test_cli.py::TestCommandDeprecation::test_deprecated_commands_parsed_to_current_commands[assign-peer-reviews-assign-reviews-sys_args0] tests/unit_tests/test_cli.py::TestCommandDeprecation::test_deprecated_commands_parsed_to_current_commands[purge-peer-review-teams-purge-review-teams-sys_args1] tests/unit_tests/test_cli.py::TestCommandDeprecation::test_deprecated_commands_parsed_to_current_commands[check-peer-review-progress-check-reviews-sys_args2] tests/unit_tests/test_command.py::TestSetupStudentRepos::test_raises_on_clone_failure tests/unit_tests/test_command.py::TestSetupStudentRepos::test_raises_on_duplicate_master_urls tests/unit_tests/test_command.py::TestSetupStudentRepos::test_happy_path tests/unit_tests/test_command.py::TestUpdateStudentRepos::test_raises_on_duplicate_master_urls tests/unit_tests/test_command.py::TestUpdateStudentRepos::test_happy_path tests/unit_tests/test_command.py::TestUpdateStudentRepos::test_issues_on_exceptions[issue0] tests/unit_tests/test_command.py::TestUpdateStudentRepos::test_issues_on_exceptions[None] tests/unit_tests/test_command.py::TestUpdateStudentRepos::test_issues_arent_opened_on_exceptions_if_unspeficied tests/unit_tests/test_command.py::TestOpenIssue::test_happy_path tests/unit_tests/test_command.py::TestCloseIssue::test_happy_path tests/unit_tests/test_command.py::TestCloneRepos::test_happy_path tests/unit_tests/test_command.py::TestCloneRepos::test_executes_act_hooks tests/unit_tests/test_command.py::TestMigrateRepo::test_happy_path tests/unit_tests/test_command.py::TestListIssues::test_happy_path[None-True--IssueState.OPEN] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[None-True--IssueState.CLOSED] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[None-True--IssueState.ALL] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[None-True-^.*$-IssueState.OPEN] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[None-True-^.*$-IssueState.CLOSED] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[None-True-^.*$-IssueState.ALL] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[None-False--IssueState.OPEN] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[None-False--IssueState.CLOSED] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[None-False--IssueState.ALL] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[None-False-^.*$-IssueState.OPEN] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[None-False-^.*$-IssueState.CLOSED] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[None-False-^.*$-IssueState.ALL] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[slarse-True--IssueState.OPEN] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[slarse-True--IssueState.CLOSED] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[slarse-True--IssueState.ALL] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[slarse-True-^.*$-IssueState.OPEN] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[slarse-True-^.*$-IssueState.CLOSED] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[slarse-True-^.*$-IssueState.ALL] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[slarse-False--IssueState.OPEN] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[slarse-False--IssueState.CLOSED] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[slarse-False--IssueState.ALL] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[slarse-False-^.*$-IssueState.OPEN] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[slarse-False-^.*$-IssueState.CLOSED] tests/unit_tests/test_command.py::TestListIssues::test_happy_path[slarse-False-^.*$-IssueState.ALL] tests/unit_tests/test_command.py::test_purge_review_teams tests/unit_tests/test_command.py::TestAssignPeerReviewers::test_too_few_students_raises[3-3] tests/unit_tests/test_command.py::TestAssignPeerReviewers::test_too_few_students_raises[3-4] tests/unit_tests/test_command.py::TestAssignPeerReviewers::test_zero_reviews_raises tests/unit_tests/test_command.py::TestAssignPeerReviewers::test_happy_path tests/unit_tests/test_command.py::TestCheckPeerReviewProgress::test_happy_path tests/unit_tests/test_github_api.py::TestEnsureTeamsAndMembers::test_no_previous_teams tests/unit_tests/test_github_api.py::TestEnsureTeamsAndMembers::test_all_teams_exist_but_without_members tests/unit_tests/test_github_api.py::TestEnsureTeamsAndMembers::test_raises_on_non_422_exception[unexpected_exc0] tests/unit_tests/test_github_api.py::TestEnsureTeamsAndMembers::test_raises_on_non_422_exception[unexpected_exc1] tests/unit_tests/test_github_api.py::TestEnsureTeamsAndMembers::test_raises_on_non_422_exception[unexpected_exc2] tests/unit_tests/test_github_api.py::TestEnsureTeamsAndMembers::test_running_twice_has_no_ill_effects tests/unit_tests/test_github_api.py::TestCreateRepos::test_creates_correct_repos tests/unit_tests/test_github_api.py::TestCreateRepos::test_skips_existing_repos tests/unit_tests/test_github_api.py::TestCreateRepos::test_raises_on_unexpected_error[unexpected_exception0] tests/unit_tests/test_github_api.py::TestCreateRepos::test_raises_on_unexpected_error[unexpected_exception1] tests/unit_tests/test_github_api.py::TestCreateRepos::test_raises_on_unexpected_error[unexpected_exception2] tests/unit_tests/test_github_api.py::TestCreateRepos::test_returns_all_urls tests/unit_tests/test_github_api.py::TestCreateRepos::test_create_repos_without_team_id tests/unit_tests/test_github_api.py::TestGetRepoUrls::test_with_token_without_user tests/unit_tests/test_github_api.py::TestGetRepoUrls::test_with_token_and_user tests/unit_tests/test_github_api.py::TestGetRepoUrls::test_with_students tests/unit_tests/test_github_api.py::TestOpenIssue::test_on_existing_repos tests/unit_tests/test_github_api.py::TestOpenIssue::test_on_some_non_existing_repos tests/unit_tests/test_github_api.py::TestOpenIssue::test_no_crash_when_no_repos_are_found tests/unit_tests/test_github_api.py::TestCloseIssue::test_closes_correct_issues tests/unit_tests/test_github_api.py::TestCloseIssue::test_no_crash_if_no_repos_found tests/unit_tests/test_github_api.py::TestCloseIssue::test_no_crash_if_no_issues_found tests/unit_tests/test_github_api.py::TestGetIssues::test_get_all_open_issues tests/unit_tests/test_github_api.py::TestGetIssues::test_get_all_closed_issues tests/unit_tests/test_github_api.py::TestGetIssues::test_get_issues_when_one_repo_doesnt_exist tests/unit_tests/test_github_api.py::TestGetIssues::test_get_open_issues_by_regex tests/unit_tests/test_github_api.py::TestAddReposToReviewTeams::test_with_default_issue tests/unit_tests/test_github_api.py::TestAddReposToTeams::test_happy_path tests/unit_tests/test_github_api.py::TestDeleteTeams::test_delete_non_existing_teams_does_not_crash tests/unit_tests/test_github_api.py::TestDeleteTeams::test_delete_existing_teams tests/unit_tests/test_github_api.py::TestVerifySettings::test_happy_path tests/unit_tests/test_github_api.py::TestVerifySettings::test_empty_token_raises_bad_credentials tests/unit_tests/test_github_api.py::TestVerifySettings::test_incorrect_info_raises_not_found_error[get_user] tests/unit_tests/test_github_api.py::TestVerifySettings::test_incorrect_info_raises_not_found_error[get_organization] tests/unit_tests/test_github_api.py::TestVerifySettings::test_bad_token_scope_raises tests/unit_tests/test_github_api.py::TestVerifySettings::test_not_owner_raises tests/unit_tests/test_github_api.py::TestVerifySettings::test_raises_unexpected_exception_on_unexpected_status tests/unit_tests/test_github_api.py::TestVerifySettings::test_none_user_raises tests/unit_tests/test_github_api.py::TestVerifySettings::test_mismatching_user_login_raises tests/unit_tests/test_github_api.py::TestGetPeerReviewProgress::test_nothing_returns tests/unit_tests/test_github_api.py::TestGetPeerReviewProgress::test_with_review_teams_but_no_repos
repobee/cli.py repobee/gitlab_api.py repobee/github_api.py repobee/apimeta.py
repobee/cli.py repobee/gitlab_api.py repobee/github_api.py repobee/apimeta.py