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

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

```diff
@@            Coverage Diff            @@
##           master    #363      +/-   ##
=========================================
- Coverage   87.81%   87.6%   -0.21%     
=========================================
  Files          21      21              
  Lines        1608    1581      -27     
  Branches      292     289       -3     
=========================================
- Hits         1412    1385      -27     
  Misses        172     172              
  Partials       24      24
```


| [Impacted Files](https://codecov.io/gh/repobee/repobee/pull/363?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/\_repobee/cli.py](https://codecov.io/gh/repobee/repobee/pull/363/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2NsaS5weQ==) | `98.09% <100%> (-0.16%)` | :arrow_down: |
| [src/\_repobee/main.py](https://codecov.io/gh/repobee/repobee/pull/363/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL21haW4ucHk=) | `100% <100%> (ø)` | :arrow_up: |

------

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

diff --git a/src/_repobee/cli.py b/src/_repobee/cli.py
index fbb2143..5256e34 100644
--- a/src/_repobee/cli.py
+++ b/src/_repobee/cli.py
@@ -14,7 +14,6 @@ import pathlib
 import os
 import sys
 import re
-from contextlib import contextmanager
 from typing import List, Iterable, Optional, Tuple
 
 import logging
@@ -283,37 +282,28 @@ def dispatch_command(
     hook_results = {}
     if ext_command_names and args.subparser in ext_command_names:
         ext_cmd = ext_commands[ext_command_names.index(args.subparser)]
-        with _sys_exit_on_expected_error():
-            ext_cmd.callback(args, api)
+        ext_cmd.callback(args, api)
     elif args.subparser == SETUP_PARSER:
-        with _sys_exit_on_expected_error():
-            command.setup_student_repos(
-                args.master_repo_urls, args.students, api
-            )
+        command.setup_student_repos(args.master_repo_urls, args.students, api)
     elif args.subparser == UPDATE_PARSER:
-        with _sys_exit_on_expected_error():
-            command.update_student_repos(
-                args.master_repo_urls, args.students, api, issue=args.issue
-            )
+        command.update_student_repos(
+            args.master_repo_urls, args.students, api, issue=args.issue
+        )
     elif args.subparser == OPEN_ISSUE_PARSER:
-        with _sys_exit_on_expected_error():
-            command.open_issue(
-                args.issue, args.master_repo_names, args.students, api
-            )
+        command.open_issue(
+            args.issue, args.master_repo_names, args.students, api
+        )
     elif args.subparser == CLOSE_ISSUE_PARSER:
-        with _sys_exit_on_expected_error():
-            command.close_issue(
-                args.title_regex, args.master_repo_names, args.students, api
-            )
+        command.close_issue(
+            args.title_regex, args.master_repo_names, args.students, api
+        )
     elif args.subparser == MIGRATE_PARSER:
-        with _sys_exit_on_expected_error():
-            command.migrate_repos(args.master_repo_urls, api)
+        command.migrate_repos(args.master_repo_urls, api)
     elif args.subparser == CLONE_PARSER:
-        with _sys_exit_on_expected_error():
-            hook_results = command.clone_repos(
-                args.master_repo_names, args.students, api
-            )
-            LOGGER.info(formatters.format_hook_results_output(hook_results))
+        hook_results = command.clone_repos(
+            args.master_repo_names, args.students, api
+        )
+        LOGGER.info(formatters.format_hook_results_output(hook_results))
     elif args.subparser == VERIFY_PARSER:
         plug.manager.hook.get_api_class().verify_settings(
             args.user,
@@ -323,42 +313,35 @@ def dispatch_command(
             args.master_org_name,
         )
     elif args.subparser == LIST_ISSUES_PARSER:
-        with _sys_exit_on_expected_error():
-            hook_results = command.list_issues(
-                args.master_repo_names,
-                args.students,
-                api,
-                state=args.state,
-                title_regex=args.title_regex or "",
-                show_body=args.show_body,
-                author=args.author,
-            )
+        hook_results = command.list_issues(
+            args.master_repo_names,
+            args.students,
+            api,
+            state=args.state,
+            title_regex=args.title_regex or "",
+            show_body=args.show_body,
+            author=args.author,
+        )
     elif args.subparser == ASSIGN_REVIEWS_PARSER:
-        with _sys_exit_on_expected_error():
-            command.assign_peer_reviews(
-                args.master_repo_names,
-                args.students,
-                args.num_reviews,
-                args.issue,
-                api,
-            )
+        command.assign_peer_reviews(
+            args.master_repo_names,
+            args.students,
+            args.num_reviews,
+            args.issue,
+            api,
+        )
     elif args.subparser == PURGE_REVIEW_TEAMS_PARSER:
-        with _sys_exit_on_expected_error():
-            command.purge_review_teams(
-                args.master_repo_names, args.students, api
-            )
+        command.purge_review_teams(args.master_repo_names, args.students, api)
     elif args.subparser == SHOW_CONFIG_PARSER:
-        with _sys_exit_on_expected_error():
-            command.show_config()
+        command.show_config()
     elif args.subparser == CHECK_REVIEW_PROGRESS_PARSER:
-        with _sys_exit_on_expected_error():
-            command.check_peer_review_progress(
-                args.master_repo_names,
-                args.students,
-                args.title_regex,
-                args.num_reviews,
-                api,
-            )
+        command.check_peer_review_progress(
+            args.master_repo_names,
+            args.students,
+            args.title_regex,
+            args.num_reviews,
+            api,
+        )
     else:
         raise exception.ParseError(
             "Illegal value for subparser: {}. "
@@ -1006,30 +989,6 @@ def _add_traceback_arg(parser):
     )
 
 
-@contextmanager
-def _sys_exit_on_expected_error():
-    try:
-        yield
-    except exception.PushFailedError as exc:
-        LOGGER.error(
-            "There was an error pushing to {}. "
-            "Verify that your token has adequate access.".format(exc.url)
-        )
-        sys.exit(1)
-    except exception.CloneFailedError as exc:
-        LOGGER.error(
-            "There was an error cloning from {}. "
-            "Does the repo really exist?".format(exc.url)
-        )
-        sys.exit(1)
-    except exception.GitError:
-        LOGGER.error("Something went wrong with git. See the logs for info.")
-        sys.exit(1)
-    except exception.APIError as exc:
-        LOGGER.error("Exiting beacuse of {.__class__.__name__}".format(exc))
-        sys.exit(1)
-
-
 def _extract_groups(args: argparse.Namespace) -> List[str]:
     """Extract groups from args namespace.`
 
diff --git a/src/_repobee/main.py b/src/_repobee/main.py
index 6aec08f..946bb44 100644
--- a/src/_repobee/main.py
+++ b/src/_repobee/main.py
@@ -91,6 +91,7 @@ def main(sys_args: List[str]):
             LOGGER.exception("Critical exception")
         else:
             LOGGER.error("{.__class__.__name__}: {}".format(exc, str(exc)))
+        sys.exit(1)
 
 
 if __name__ == "__main__":

Remove _sys_exit_on_expected_error context manager in cli
It just removes information, I have no idea of why I put it there in the first place.
diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py
index 3f21414..8b8724d 100644
--- a/tests/unit_tests/test_cli.py
+++ b/tests/unit_tests/test_cli.py
@@ -267,20 +267,6 @@ class TestDispatchCommand:
         that there are no crashes, does not validate any other behavior!"""
         cli.dispatch_command(parsed_args_all_subparsers, api_instance_mock)
 
-    def test_expected_exception_results_in_system_exit(
-        self,
-        parsed_args_all_subparsers,
-        api_instance_mock,
-        command_all_raise_mock,
-    ):
-        """Test that any of the expected exceptions results in SystemExit."""
-        if parsed_args_all_subparsers.subparser == cli.VERIFY_PARSER:
-            pytest.skip(
-                "verify-settings parser is exempt from the sysexit behavior"
-            )
-        with pytest.raises(SystemExit):
-            cli.dispatch_command(parsed_args_all_subparsers, api_instance_mock)
-
     def test_setup_student_repos_called_with_correct_args(
         self, command_mock, api_instance_mock
     ):
diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py
index e750823..7bd11c5 100644
--- a/tests/unit_tests/test_main.py
+++ b/tests/unit_tests/test_main.py
@@ -97,23 +97,24 @@ def test_happy_path(
     )
 
 
-def test_does_not_raise_on_exception_in_parsing(
+def test_exit_status_1_on_exception_in_parsing(
     api_instance_mock, parse_args_mock, dispatch_command_mock, no_config_mock
 ):
-    """should just log, but not raise."""
     msg = "some nice error message"
     parse_args_mock.side_effect = raise_(Exception(msg))
     sys_args = ["just some made up arguments".split()]
 
-    main.main(sys_args)
+    with pytest.raises(SystemExit) as exc_info:
+        main.main(sys_args)
 
+    assert exc_info.value.code == 1
     parse_args_mock.assert_called_once_with(
         sys_args[1:], show_all_opts=False, ext_commands=DEFAULT_EXT_COMMANDS
     )
     assert not dispatch_command_mock.called
 
 
-def test_does_not_raise_on_exception_in_handling_parsed_args(
+def test_exit_status_1_on_exception_in_handling_parsed_args(
     api_instance_mock, parse_args_mock, dispatch_command_mock
 ):
     """should just log, but not raise."""
@@ -121,7 +122,13 @@ def test_does_not_raise_on_exception_in_handling_parsed_args(
     dispatch_command_mock.side_effect = raise_(Exception(msg))
     sys_args = ["just some made up arguments".split()]
 
-    main.main(sys_args)
+    with pytest.raises(SystemExit) as exc_info:
+        main.main(sys_args)
+
+    assert exc_info.value.code == 1
+    parse_args_mock.assert_called_once_with(
+        sys_args[1:], show_all_opts=False, ext_commands=DEFAULT_EXT_COMMANDS
+    )
 
 
 def test_plugins_args(
@@ -271,8 +278,10 @@ def test_logs_traceback_on_exception_in_dispatch_if_traceback(
     sys_args = ["repobee", *CLONE_ARGS, "--traceback"]
     dispatch_command_mock.side_effect = lambda *args, **kwargs: raise_()
 
-    main.main(sys_args)
+    with pytest.raises(SystemExit) as exc_info:
+        main.main(sys_args)
 
+    assert exc_info.value.code == 1
     assert logger_exception_mock.called
     parse_args_mock.assert_called_once_with(
         [*CLONE_ARGS, "--traceback"],
@@ -312,3 +321,19 @@ def test_show_all_opts_correctly_separated(
     parse_preparser_options_mock.assert_called_once_with(
         [cli.PRE_PARSER_SHOW_ALL_OPTS]
     )
+
+
+def test_non_zero_exit_status_on_exception(
+    parse_args_mock, parse_preparser_options_mock, no_config_mock
+):
+    def raise_(*args, **kwargs):
+        raise ValueError()
+
+    parse_args_mock.side_effect = raise_
+
+    sys_args = ["repobee", *CLONE_ARGS]
+
+    with pytest.raises(SystemExit) as exc_info:
+        main.main(sys_args)
+
+    assert exc_info.value.code == 1

pip install -e .[TEST]
parse_log_pytest
requirements.txt
pytest
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
backports.zoneinfo==0.2.1
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
dateparser==1.2.0
Deprecated==1.2.18
exceptiongroup==1.2.2
humanize==4.6.0
idna==3.10
importlib-metadata==6.7.0
iniconfig==2.0.0
maya==0.6.1
packaging==24.0
pendulum==2.1.2
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-dateutil==2.9.0.post0
python-gitlab==1.9.0
python-json-logger==3.0.1
pytz==2025.2
pytzdata==2020.1
regex==2024.4.16
-e git+https://github.com/repobee/repobee.git@47fe1df45f8b1805337090591fa372ae77c9fca2#egg=repobee
repobee-plug==0.10.0
requests==2.31.0
six==1.17.0
snaptime==0.2.4
tomli==2.0.1
typing_extensions==4.7.1
tzlocal==5.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
      - backports-zoneinfo==0.2.1
      - 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
      - dateparser==1.2.0
      - deprecated==1.2.18
      - exceptiongroup==1.2.2
      - humanize==4.6.0
      - idna==3.10
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - maya==0.6.1
      - packaging==24.0
      - pendulum==2.1.2
      - 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-dateutil==2.9.0.post0
      - python-gitlab==1.9.0
      - python-json-logger==3.0.1
      - pytz==2025.2
      - pytzdata==2020.1
      - regex==2024.4.16
      - repobee==2.2.0
      - repobee-plug==0.10.0
      - requests==2.31.0
      - six==1.17.0
      - snaptime==0.2.4
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - tzlocal==5.1
      - urllib3==2.0.7
      - wrapt==1.16.0
      - zipp==3.15.0
prefix: /opt/conda/envs/repobee

tests/unit_tests/test_main.py::test_exit_status_1_on_exception_in_parsing tests/unit_tests/test_main.py::test_exit_status_1_on_exception_in_handling_parsed_args tests/unit_tests/test_main.py::test_logs_traceback_on_exception_in_dispatch_if_traceback tests/unit_tests/test_main.py::test_non_zero_exit_status_on_exception
tests/unit_tests/test_cli.py::TestDispatchCommand::test_writes_hook_results_correctly[clone-_repobee.command.clone_repos] tests/unit_tests/test_cli.py::TestDispatchCommand::test_writes_hook_results_correctly[list-issues-_repobee.command.list_issues] tests/unit_tests/test_cli.py::TestDispatchCommand::test_does_not_write_hook_results_if_there_are_none 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[end-reviews] 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_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[end-reviews] 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::test_create_parser_for_docs tests/unit_tests/test_cli.py::TestBaseParsing::test_show_all_opts_true_shows_configured_args tests/unit_tests/test_cli.py::TestBaseParsing::test_show_all_opts_false_hides_configured_args 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::TestExtensionCommands::test_parse_ext_command_that_does_not_require_api tests/unit_tests/test_cli.py::TestExtensionCommands::test_parse_ext_command_that_requires_api tests/unit_tests/test_cli.py::TestExtensionCommands::test_dispatch_ext_command_that_does_not_require_api tests/unit_tests/test_cli.py::TestExtensionCommands::test_dispatch_ext_command_that_requires_api tests/unit_tests/test_cli.py::TestExtensionCommands::test_parse_ext_command_that_requires_base_parsers tests/unit_tests/test_cli.py::TestStudentParsing::test_raises_if_students_file_is_not_a_file[setup|['--mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_raises_if_students_file_is_not_a_file[update|['--mn', 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|['--mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_parser_listing_students[update|['--mn', 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|['--mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_parser_student_file[update|['--mn', 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|['--mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_student_parsers_raise_on_empty_student_file[update|['--mn', 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|['--mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_parsers_raise_if_both_file_and_listing[update|['--mn', 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|['--mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_student_groups_parsed_correcly[update|['--mn', 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|['--mn', tests/unit_tests/test_cli.py::TestStudentParsing::test_raises_if_generated_team_name_too_long[update|['--mn', 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[--bu] 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[--bu] 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-end-reviews-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_main.py::test_happy_path tests/unit_tests/test_main.py::test_plugins_args tests/unit_tests/test_main.py::test_no_plugins_arg tests/unit_tests/test_main.py::test_no_plugins_with_configured_plugins tests/unit_tests/test_main.py::test_configured_plugins_are_loaded tests/unit_tests/test_main.py::test_plugin_with_subparser_name tests/unit_tests/test_main.py::test_plug_arg_incompatible_with_no_plugins tests/unit_tests/test_main.py::test_invalid_plug_options tests/unit_tests/test_main.py::test_does_not_raise_on_exception_in_dispatch tests/unit_tests/test_main.py::test_show_all_opts_correctly_separated
src/_repobee/main.py src/_repobee/cli.py
src/_repobee/main.py src/_repobee/cli.py