Repository: repobee/repobee

Issue:
The preparser needs a --config-file option
To specify which config file to use. It's really quite essential to do system testing of plugins.

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

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

```diff
@@            Coverage Diff             @@
##           master     #440      +/-   ##
==========================================
- Coverage   91.42%   86.70%   -4.72%     
==========================================
  Files          28       28              
  Lines        1819     1813       -6     
  Branches      339      338       -1     
==========================================
- Hits         1663     1572      -91     
- Misses        124      206      +82     
- Partials       32       35       +3     
```


| [Impacted Files](https://codecov.io/gh/repobee/repobee/pull/440?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/\_repobee/plugin.py](https://codecov.io/gh/repobee/repobee/pull/440/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL3BsdWdpbi5weQ==) | `91.42% <ø> (-0.17%)` | :arrow_down: |
| [src/\_repobee/command/repos.py](https://codecov.io/gh/repobee/repobee/pull/440/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2NvbW1hbmQvcmVwb3MucHk=) | `91.80% <25.00%> (-3.32%)` | :arrow_down: |
| [src/\_repobee/cli/dispatch.py](https://codecov.io/gh/repobee/repobee/pull/440/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2NsaS9kaXNwYXRjaC5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/\_repobee/cli/mainparser.py](https://codecov.io/gh/repobee/repobee/pull/440/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2NsaS9tYWlucGFyc2VyLnB5) | `96.42% <100.00%> (+0.02%)` | :arrow_up: |
| [src/\_repobee/cli/parsing.py](https://codecov.io/gh/repobee/repobee/pull/440/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2NsaS9wYXJzaW5nLnB5) | `92.26% <100.00%> (-1.20%)` | :arrow_down: |
| [src/\_repobee/cli/preparser.py](https://codecov.io/gh/repobee/repobee/pull/440/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2NsaS9wcmVwYXJzZXIucHk=) | `100.00% <100.00%> (ø)` | |
| [src/\_repobee/config.py](https://codecov.io/gh/repobee/repobee/pull/440/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2NvbmZpZy5weQ==) | `98.52% <100.00%> (ø)` | |
| [src/\_repobee/main.py](https://codecov.io/gh/repobee/repobee/pull/440/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL21haW4ucHk=) | `91.30% <100.00%> (+4.07%)` | :arrow_up: |
| [src/\_repobee/ext/gitlab.py](https://codecov.io/gh/repobee/repobee/pull/440/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2V4dC9naXRsYWIucHk=) | `63.86% <0.00%> (-28.56%)` | :arrow_down: |
| [src/\_repobee/git.py](https://codecov.io/gh/repobee/repobee/pull/440/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2dpdC5weQ==) | `96.62% <0.00%> (-3.38%)` | :arrow_down: |
| ... and [6 more](https://codecov.io/gh/repobee/repobee/pull/440/diff?src=pr&el=tree-more) | |

------

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


Patch:
diff --git a/src/_repobee/cli/dispatch.py b/src/_repobee/cli/dispatch.py
index b600d98..bcf99e8 100644
--- a/src/_repobee/cli/dispatch.py
+++ b/src/_repobee/cli/dispatch.py
@@ -36,6 +36,7 @@ from _repobee.cli.mainparser import (
 def dispatch_command(
     args: argparse.Namespace,
     api: plug.API,
+    config_file: pathlib.Path,
     ext_commands: Optional[List[plug.ExtensionCommand]] = None,
 ):
     """Handle parsed CLI arguments and dispatch commands to the appropriate
@@ -45,6 +46,7 @@ def dispatch_command(
     Args:
         args: A namespace of parsed command line arguments.
         api: An initialized plug.API instance.
+        config_file: Path to the config file.
         ext_commands: A list of active extension commands.
     """
     hook_results = {}
@@ -101,7 +103,7 @@ def dispatch_command(
     elif args.subparser == PURGE_REVIEW_TEAMS_PARSER:
         command.purge_review_teams(args.master_repo_names, args.students, api)
     elif args.subparser == SHOW_CONFIG_PARSER:
-        command.show_config()
+        command.show_config(config_file)
     elif args.subparser == CHECK_REVIEW_PROGRESS_PARSER:
         command.check_peer_review_progress(
             args.master_repo_names,
diff --git a/src/_repobee/cli/mainparser.py b/src/_repobee/cli/mainparser.py
index 0320e86..2523bd1 100644
--- a/src/_repobee/cli/mainparser.py
+++ b/src/_repobee/cli/mainparser.py
@@ -8,6 +8,7 @@
 
 import types
 import argparse
+import pathlib
 
 import logging
 from typing import List, Optional
@@ -123,11 +124,17 @@ def create_parser_for_docs() -> argparse.ArgumentParser:
     # load default plugins
     plugin.initialize_plugins([_repobee.constants.DEFAULT_PLUGIN])
     ext_commands = plug.manager.hook.create_extension_command()
-    return create_parser(show_all_opts=True, ext_commands=ext_commands)
+    return create_parser(
+        show_all_opts=True,
+        ext_commands=ext_commands,
+        config_file=_repobee.constants.DEFAULT_CONFIG_FILE,
+    )
 
 
 def create_parser(
-    show_all_opts: bool, ext_commands: Optional[List[plug.ExtensionCommand]]
+    show_all_opts: bool,
+    ext_commands: Optional[List[plug.ExtensionCommand]],
+    config_file: pathlib.Path,
 ) -> argparse.ArgumentParser:
     """Create the primary parser.
 
@@ -135,6 +142,7 @@ def create_parser(
         show_all_opts: If False, help sections for options with configured
             defaults are suppressed. Otherwise, all options are shown.
         ext_commands: A list of extension commands.
+        config_file: Path to the config file.
     Returns:
         The primary parser.
     """
@@ -181,7 +189,7 @@ def create_parser(
         action="version",
         version="{}".format(_repobee.__version__),
     )
-    _add_subparsers(parser, show_all_opts, ext_commands)
+    _add_subparsers(parser, show_all_opts, ext_commands, config_file)
 
     return parser
 
@@ -475,7 +483,7 @@ def _add_extension_parsers(
     return ext_commands
 
 
-def _add_subparsers(parser, show_all_opts, ext_commands):
+def _add_subparsers(parser, show_all_opts, ext_commands, config_file):
     """Add all of the subparsers to the parser. Note that the parsers prefixed
     with `base_` do not have any parent parsers, so any parser inheriting from
     them must also inherit from the required `base_parser` (unless it is a
@@ -483,7 +491,7 @@ def _add_subparsers(parser, show_all_opts, ext_commands):
     """
 
     base_parser, base_student_parser, master_org_parser = _create_base_parsers(
-        show_all_opts
+        show_all_opts, config_file
     )
 
     subparsers = parser.add_subparsers(dest=SUB)
@@ -619,9 +627,9 @@ def _add_subparsers(parser, show_all_opts, ext_commands):
     )
 
 
-def _create_base_parsers(show_all_opts):
+def _create_base_parsers(show_all_opts, config_file):
     """Create the base parsers."""
-    configured_defaults = config.get_configured_defaults()
+    configured_defaults = config.get_configured_defaults(config_file)
 
     def default(arg_name):
         return (
diff --git a/src/_repobee/cli/parsing.py b/src/_repobee/cli/parsing.py
index 1364074..1cec0a2 100644
--- a/src/_repobee/cli/parsing.py
+++ b/src/_repobee/cli/parsing.py
@@ -46,6 +46,7 @@ def handle_args(
     sys_args: Iterable[str],
     show_all_opts: bool = False,
     ext_commands: Optional[List[plug.ExtensionCommand]] = None,
+    config_file: pathlib.Path = constants.DEFAULT_CONFIG_FILE,
 ) -> Tuple[argparse.Namespace, Optional[plug.API]]:
     """Parse and process command line arguments and instantiate the platform
     API (if it's needed).
@@ -53,6 +54,7 @@ def handle_args(
 
     Args:
         sys_args: Raw command line arguments for the primary parser.
+        config_file: Path to the config file.
         show_all_opts: If False, help sections for options that have
             configured defaults are suppressed. Otherwise, all options are
             shown.
@@ -62,7 +64,9 @@ def handle_args(
         A tuple of a namespace with parsed and processed arguments, and an
         instance of the platform API if it is required for the command.
     """
-    args, processing = _parse_args(sys_args, show_all_opts, ext_commands)
+    args, processing = _parse_args(
+        sys_args, config_file, show_all_opts, ext_commands
+    )
     if processing == _ArgsProcessing.CORE:
         processed_args, api = _process_args(args, ext_commands)
         _handle_task_parsing(processed_args)
@@ -75,6 +79,7 @@ def handle_args(
 
 def _parse_args(
     sys_args: Iterable[str],
+    config_file: pathlib.Path,
     show_all_opts: bool = False,
     ext_commands: Optional[List[plug.ExtensionCommand]] = None,
 ) -> Tuple[argparse.Namespace, _ArgsProcessing]:
@@ -84,6 +89,7 @@ def _parse_args(
 
     Args:
         sys_args: A list of command line arguments.
+        config_file: Path to the config file.
         show_all_opts: If False, CLI arguments that are configure in the
             configuration file are not shown in help menus.
         ext_commands: A list of extension commands.
@@ -92,7 +98,7 @@ def _parse_args(
         A namespace of parsed arpuments and a boolean that specifies whether or
         not further processing is required.
     """
-    parser = create_parser(show_all_opts, ext_commands)
+    parser = create_parser(show_all_opts, ext_commands, config_file)
     args = parser.parse_args(_handle_deprecation(sys_args))
 
     ext_cmd = _resolve_extension_command(args.subparser, ext_commands)
diff --git a/src/_repobee/cli/preparser.py b/src/_repobee/cli/preparser.py
index d098610..c580802 100644
--- a/src/_repobee/cli/preparser.py
+++ b/src/_repobee/cli/preparser.py
@@ -14,13 +14,17 @@ possible if a separate parser runs before the primary parser.
 .. moduleauthor:: Simon Larsén
 """
 
-# any pre-parser options go here
 import argparse
+import pathlib
 from typing import List
 
 import _repobee.cli
+import _repobee.constants
 
 PRE_PARSER_PLUG_OPTS = ["-p", "--plug"]
+PRE_PARSER_CONFIG_OPTS = ["-c", "--config-file"]
+PRE_PARSER_OPTS = PRE_PARSER_PLUG_OPTS + PRE_PARSER_CONFIG_OPTS
+
 PRE_PARSER_NO_PLUGS = "--no-plugins"
 PRE_PARSER_SHOW_ALL_OPTS = "--show-all-opts"
 # this list should include all pre-parser flags
@@ -40,6 +44,13 @@ def parse_args(sys_args: List[str]) -> argparse.Namespace:
         prog="repobee", description="plugin pre-parser for _repobee."
     )
 
+    parser.add_argument(
+        *PRE_PARSER_CONFIG_OPTS,
+        help="Specify path to the config file to use.",
+        type=pathlib.Path,
+        default=_repobee.constants.DEFAULT_CONFIG_FILE,
+    )
+
     mutex_grp = parser.add_mutually_exclusive_group()
     mutex_grp.add_argument(
         *PRE_PARSER_PLUG_OPTS,
@@ -74,7 +85,7 @@ def separate_args(args: List[str]) -> (List[str], List[str]):
     if args and args[0].startswith("-"):
         cur = 0
         while cur < len(args) and args[cur].startswith("-"):
-            if args[cur] in _repobee.cli.preparser.PRE_PARSER_PLUG_OPTS:
+            if args[cur] in _repobee.cli.preparser.PRE_PARSER_OPTS:
                 preparser_args += args[cur : cur + 2]
                 cur += 2
             elif args[cur] in _repobee.cli.preparser.PRE_PARSER_FLAGS:
diff --git a/src/_repobee/command/repos.py b/src/_repobee/command/repos.py
index 0fa74e4..e959aa8 100644
--- a/src/_repobee/command/repos.py
+++ b/src/_repobee/command/repos.py
@@ -28,7 +28,6 @@ from _repobee import git
 from _repobee import util
 from _repobee import exception
 from _repobee import config
-from _repobee import constants
 from _repobee import plugin
 from _repobee.git import Push
 
@@ -351,16 +350,12 @@ def _create_push_tuples(
     return push_tuples
 
 
-def show_config() -> None:
+def show_config(config_file: pathlib.Path) -> None:
     """Print the configuration file to the log."""
-    config.check_config_integrity()
+    config.check_config_integrity(config_file)
 
-    LOGGER.info(
-        "Found valid config file at " + str(constants.DEFAULT_CONFIG_FILE)
-    )
-    with constants.DEFAULT_CONFIG_FILE.open(
-        encoding=sys.getdefaultencoding()
-    ) as f:
+    LOGGER.info(f"Found valid config file at {config_file}")
+    with config_file.open(encoding=sys.getdefaultencoding()) as f:
         config_contents = "".join(f.readlines())
 
     output = (
diff --git a/src/_repobee/config.py b/src/_repobee/config.py
index 409ceb2..1a1b194 100644
--- a/src/_repobee/config.py
+++ b/src/_repobee/config.py
@@ -23,9 +23,7 @@ from _repobee import constants
 LOGGER = daiquiri.getLogger(__file__)
 
 
-def get_configured_defaults(
-    config_file: Union[str, pathlib.Path] = constants.DEFAULT_CONFIG_FILE
-) -> dict:
+def get_configured_defaults(config_file: Union[str, pathlib.Path]) -> dict:
     """Access the config file and return a ConfigParser instance with
     its contents.
 
@@ -63,9 +61,7 @@ def check_defaults(
         )
 
 
-def get_plugin_names(
-    config_file: Union[str, pathlib.Path] = constants.DEFAULT_CONFIG_FILE
-) -> List[str]:
+def get_plugin_names(config_file: Union[str, pathlib.Path]) -> List[str]:
     """Return a list of unqualified names of plugins listed in the config. The
     order of the plugins is preserved.
 
@@ -90,9 +86,7 @@ def get_plugin_names(
     return [name.strip() for name in plugin_string.split(",") if name]
 
 
-def execute_config_hooks(
-    config_file: Union[str, pathlib.Path] = constants.DEFAULT_CONFIG_FILE
-) -> None:
+def execute_config_hooks(config_file: Union[str, pathlib.Path]) -> None:
     """Execute all config hooks.
 
     Args:
@@ -105,9 +99,7 @@ def execute_config_hooks(
     plug.manager.hook.config_hook(config_parser=config_parser)
 
 
-def check_config_integrity(
-    config_file: Union[str, pathlib.Path] = constants.DEFAULT_CONFIG_FILE
-) -> None:
+def check_config_integrity(config_file: Union[str, pathlib.Path]) -> None:
     """Raise an exception if the configuration file contains syntactical
     errors, or if the defaults are misconfigured. Note that plugin options are
     not checked.
@@ -158,9 +150,7 @@ def _fetch_token() -> Optional[str]:
     return token or token_from_old
 
 
-def _read_defaults(
-    config_file: pathlib.Path = constants.DEFAULT_CONFIG_FILE,
-) -> dict:
+def _read_defaults(config_file: pathlib.Path) -> dict:
     token = _fetch_token()
     if not config_file.is_file():
         return {} if not token else dict(token=token)
@@ -175,9 +165,7 @@ def _read_defaults(
     return defaults
 
 
-def _read_config(
-    config_file: pathlib.Path = constants.DEFAULT_CONFIG_FILE,
-) -> configparser.ConfigParser:
+def _read_config(config_file: pathlib.Path) -> configparser.ConfigParser:
     config_parser = configparser.ConfigParser()
     try:
         config_parser.read(str(config_file))
diff --git a/src/_repobee/main.py b/src/_repobee/main.py
index be5ce7b..1c9afde 100644
--- a/src/_repobee/main.py
+++ b/src/_repobee/main.py
@@ -44,13 +44,14 @@ def main(sys_args: List[str]):
         parsed_preparser_args = _repobee.cli.preparser.parse_args(
             preparser_args
         )
+        config_file = parsed_preparser_args.config_file
 
         if parsed_preparser_args.no_plugins:
             LOGGER.info("Non-default plugins disabled")
             plugin.initialize_plugins([constants.DEFAULT_PLUGIN])
         else:
             plugin_names = plugin.resolve_plugin_names(
-                parsed_preparser_args.plug, constants.DEFAULT_CONFIG_FILE
+                parsed_preparser_args.plug, config_file
             )
             # IMPORTANT: the default plugin MUST be loaded last to ensure that
             # any user-defined plugins override the firstresult hooks
@@ -58,16 +59,19 @@ def main(sys_args: List[str]):
                 plugin_names + [constants.DEFAULT_PLUGIN]
             )
 
-        config.execute_config_hooks()
+        config.execute_config_hooks(config_file)
         ext_commands = plug.manager.hook.create_extension_command()
         parsed_args, api = _repobee.cli.parsing.handle_args(
             app_args,
             show_all_opts=parsed_preparser_args.show_all_opts,
             ext_commands=ext_commands,
+            config_file=config_file,
         )
         traceback = parsed_args.traceback
         pre_init = False
-        _repobee.cli.dispatch.dispatch_command(parsed_args, api, ext_commands)
+        _repobee.cli.dispatch.dispatch_command(
+            parsed_args, api, config_file, ext_commands
+        )
     except exception.PluginLoadError as exc:
         LOGGER.error("{.__class__.__name__}: {}".format(exc, str(exc)))
         LOGGER.error(
diff --git a/src/_repobee/plugin.py b/src/_repobee/plugin.py
index 7605609..c0361b6 100644
--- a/src/_repobee/plugin.py
+++ b/src/_repobee/plugin.py
@@ -23,7 +23,6 @@ import daiquiri
 import _repobee
 from _repobee import config
 from _repobee import exception
-from _repobee import constants
 
 import repobee_plug as plug
 
@@ -129,8 +128,7 @@ def initialize_plugins(plugin_names: List[str] = None):
 
 
 def resolve_plugin_names(
-    plugin_names: Optional[List[str]] = None,
-    config_file: pathlib.Path = constants.DEFAULT_CONFIG_FILE,
+    plugin_names: Optional[List[str]], config_file: pathlib.Path,
 ) -> List[str]:
     """Return a list of plugin names to load into RepoBee given a list of
     externally specified plugin names, and a path to a configuration file.


Test patch:
diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py
index ae428fb..ac28544 100644
--- a/tests/unit_tests/conftest.py
+++ b/tests/unit_tests/conftest.py
@@ -18,7 +18,7 @@ import constants
 sys.modules["github"] = MagicMock()
 
 
-import _repobee  # noqa: F402
+import _repobee  # noqa: E402
 
 EXPECTED_ENV_VARIABLES = [
     _repobee.constants.TOKEN_ENV,
@@ -52,6 +52,15 @@ def _students_file(populate: bool = True):
         yield file
 
 
+@pytest.fixture
+def unused_path():
+    with tempfile.NamedTemporaryFile() as tmpfile:
+        unused_path = pathlib.Path(tmpfile.name)
+
+    # exiting the with block destroys the temporary file
+    return unused_path
+
+
 @pytest.fixture(autouse=True)
 def unregister_plugins():
     """All plugins should be unregistered after each function."""
diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py
index 0d5fe5a..21a64a2 100644
--- a/tests/unit_tests/test_cli.py
+++ b/tests/unit_tests/test_cli.py
@@ -34,6 +34,8 @@ generate_repo_url = functions.generate_repo_url
 MASTER_ORG_NAME = constants.MASTER_ORG_NAME
 TOKEN = constants.TOKEN
 
+EMPTY_PATH = pathlib.Path(".")
+
 REPO_NAMES = ("week-1", "week-2", "week-3")
 REPO_URLS = tuple(map(lambda rn: generate_repo_url(rn, ORG_NAME), REPO_NAMES))
 
@@ -240,7 +242,9 @@ class TestDispatchCommand:
             command_func, autospec=True, return_value=hook_result_mapping
         ):
             args = argparse.Namespace(subparser=parser, **args_dict)
-            _repobee.cli.dispatch.dispatch_command(args, api_instance_mock)
+            _repobee.cli.dispatch.dispatch_command(
+                args, api_instance_mock, EMPTY_PATH
+            )
 
         write.assert_called_once_with(expected_json, expected_filepath)
 
@@ -261,7 +265,9 @@ class TestDispatchCommand:
             args = argparse.Namespace(
                 subparser=mainparser.CLONE_PARSER, **args_dict
             )
-            _repobee.cli.dispatch.dispatch_command(args, api_instance_mock)
+            _repobee.cli.dispatch.dispatch_command(
+                args, api_instance_mock, EMPTY_PATH
+            )
 
         assert not write.called
 
@@ -270,7 +276,9 @@ class TestDispatchCommand:
         args = argparse.Namespace(subparser=parser, **VALID_PARSED_ARGS)
 
         with pytest.raises(exception.ParseError) as exc_info:
-            _repobee.cli.dispatch.dispatch_command(args, api_instance_mock)
+            _repobee.cli.dispatch.dispatch_command(
+                args, api_instance_mock, EMPTY_PATH
+            )
         assert "Illegal value for subparser: {}".format(parser) in str(
             exc_info.value
         )
@@ -281,7 +289,7 @@ class TestDispatchCommand:
         """Test that valid arguments does not result in crash. Only validates
         that there are no crashes, does not validate any other behavior!"""
         _repobee.cli.dispatch.dispatch_command(
-            parsed_args_all_subparsers, api_instance_mock
+            parsed_args_all_subparsers, api_instance_mock, EMPTY_PATH
         )
 
     def test_setup_student_repos_called_with_correct_args(
@@ -291,7 +299,9 @@ class TestDispatchCommand:
             subparser=mainparser.SETUP_PARSER, **VALID_PARSED_ARGS
         )
 
-        _repobee.cli.dispatch.dispatch_command(args, api_instance_mock)
+        _repobee.cli.dispatch.dispatch_command(
+            args, api_instance_mock, EMPTY_PATH
+        )
 
         command_mock.setup_student_repos.assert_called_once_with(
             args.master_repo_urls, args.students, api_instance_mock
@@ -304,7 +314,9 @@ class TestDispatchCommand:
             subparser=mainparser.UPDATE_PARSER, **VALID_PARSED_ARGS
         )
 
-        _repobee.cli.dispatch.dispatch_command(args, api_instance_mock)
+        _repobee.cli.dispatch.dispatch_command(
+            args, api_instance_mock, EMPTY_PATH
+        )
 
         command_mock.update_student_repos.assert_called_once_with(
             args.master_repo_urls,
@@ -319,7 +331,9 @@ class TestDispatchCommand:
         )
         expected_arg = list(args.students)
 
-        _repobee.cli.dispatch.dispatch_command(args, api_instance_mock)
+        _repobee.cli.dispatch.dispatch_command(
+            args, api_instance_mock, EMPTY_PATH
+        )
 
         api_instance_mock.ensure_teams_and_members.assert_called_once_with(
             expected_arg
@@ -332,7 +346,9 @@ class TestDispatchCommand:
             subparser=mainparser.OPEN_ISSUE_PARSER, **VALID_PARSED_ARGS
         )
 
-        _repobee.cli.dispatch.dispatch_command(args, api_instance_mock)
+        _repobee.cli.dispatch.dispatch_command(
+            args, api_instance_mock, EMPTY_PATH
+        )
 
         command_mock.open_issue.assert_called_once_with(
             args.issue,
@@ -348,7 +364,9 @@ class TestDispatchCommand:
             subparser=mainparser.CLOSE_ISSUE_PARSER, **VALID_PARSED_ARGS
         )
 
-        _repobee.cli.dispatch.dispatch_command(args, api_instance_mock)
+        _repobee.cli.dispatch.dispatch_command(
+            args, api_instance_mock, EMPTY_PATH
+        )
 
         command_mock.close_issue.assert_called_once_with(
             args.title_regex, args.repos, api_instance_mock
@@ -361,7 +379,9 @@ class TestDispatchCommand:
             subparser=mainparser.MIGRATE_PARSER, **VALID_PARSED_ARGS
         )
 
-        _repobee.cli.dispatch.dispatch_command(args, api_instance_mock)
+        _repobee.cli.dispatch.dispatch_command(
+            args, api_instance_mock, EMPTY_PATH
+        )
 
         command_mock.migrate_repos.assert_called_once_with(
             args.master_repo_urls, api_instance_mock
@@ -374,7 +394,9 @@ class TestDispatchCommand:
             subparser=mainparser.CLONE_PARSER, **VALID_PARSED_ARGS
         )
 
-        _repobee.cli.dispatch.dispatch_command(args, api_instance_mock)
+        _repobee.cli.dispatch.dispatch_command(
+            args, api_instance_mock, EMPTY_PATH
+        )
 
         command_mock.clone_repos.assert_called_once_with(
             args.repos, api_instance_mock
@@ -392,7 +414,7 @@ class TestDispatchCommand:
             master_org_name=None,
         )
 
-        _repobee.cli.dispatch.dispatch_command(args, None)
+        _repobee.cli.dispatch.dispatch_command(args, None, EMPTY_PATH)
 
         api_class_mock.verify_settings.assert_called_once_with(
             args.user, args.org_name, args.base_url, TOKEN, None
@@ -408,7 +430,7 @@ class TestDispatchCommand:
             master_org_name=MASTER_ORG_NAME,
         )
 
-        _repobee.cli.dispatch.dispatch_command(args, None)
+        _repobee.cli.dispatch.dispatch_command(args, None, EMPTY_PATH)
 
         api_class_mock.verify_settings.assert_called_once_with(
             args.user, args.org_name, args.base_url, TOKEN, MASTER_ORG_NAME
@@ -779,7 +801,7 @@ class TestExtensionCommands:
         )
 
         _repobee.cli.dispatch.dispatch_command(
-            parsed_args, None, [ext_command]
+            parsed_args, None, EMPTY_PATH, [ext_command]
         )
 
         # for some reason, this completely sane assertion fails ALWAYS
@@ -815,7 +837,7 @@ class TestExtensionCommands:
         )
 
         _repobee.cli.dispatch.dispatch_command(
-            parsed_args, api_instance_mock, [ext_command]
+            parsed_args, api_instance_mock, EMPTY_PATH, [ext_command]
         )
 
         # for some reason, this completely sane assertion fails ALWAYS
diff --git a/tests/unit_tests/test_command.py b/tests/unit_tests/test_command.py
index 5245b77..7a405e9 100644
--- a/tests/unit_tests/test_command.py
+++ b/tests/unit_tests/test_command.py
@@ -495,15 +495,15 @@ class TestCloneRepos:
     """Tests for clone_repos."""
 
     @pytest.fixture
-    def register_default_plugins(self):
+    def register_default_plugins(self, unused_path):
         plugin_names = plugin.resolve_plugin_names(
-            plugin_names=constants.PLUGINS
+            plugin_names=constants.PLUGINS, config_file=unused_path
         )
         modules = plugin.load_plugin_modules(plugin_names)
         plugin.register_plugins(modules)
 
     @pytest.fixture
-    def act_mocks(self, monkeypatch, config_mock):
+    def act_mocks(self, monkeypatch, config_mock, unused_path):
         """Mocks for the act functions and method. This is a bit
         messy as the functions must be marked with the
         repobee_plug.repobee_hook decorator to be picked up by pluggy.
@@ -536,7 +536,7 @@ class TestCloneRepos:
         _repobee.ext.pylint.act(None, None)
 
         plugin_names = plugin.resolve_plugin_names(
-            plugin_names=constants.PLUGINS
+            plugin_names=constants.PLUGINS, config_file=unused_path
         )
         modules = plugin.load_plugin_modules(plugin_names)
         plugin.register_plugins(modules)
diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py
index e7e8757..7466b82 100644
--- a/tests/unit_tests/test_config.py
+++ b/tests/unit_tests/test_config.py
@@ -1,6 +1,8 @@
 import os
 from unittest import mock
+
 import pytest
+
 import _repobee.constants
 from _repobee import config
 from _repobee import exception
@@ -19,8 +21,10 @@ CONFIG_TOKEN = constants.CONFIG_TOKEN
 class TestGetConfiguredDefaults:
     """Tests for get_configured_defaults"""
 
-    def test_get_configured_defaults_no_config_file(self, isfile_mock):
-        defaults = config.get_configured_defaults()
+    def test_get_configured_defaults_no_config_file(
+        self, isfile_mock, unused_path
+    ):
+        defaults = config.get_configured_defaults(unused_path)
         assert defaults == dict(token=constants.TOKEN)
 
     def test_get_configured_defaults_empty_file(self, empty_config_mock):
@@ -153,8 +157,8 @@ class TestGetPluginNames:
 class TestExecuteConfigHooks:
     """Tests for execute_config_hooks."""
 
-    def test_with_no_config_file(self, no_config_mock, plugin_manager_mock):
-        config.execute_config_hooks()
+    def test_with_no_config_file(self, unused_path, plugin_manager_mock):
+        config.execute_config_hooks(config_file=unused_path)
         assert not plugin_manager_mock.hook.config_hook.called
 
     def test_with_config_file(self, config_mock, plugin_manager_mock):
@@ -180,13 +184,11 @@ class TestCheckConfigIntegrity:
             + os.linesep.join(["[some_config]", "option = value", "bla = blu"])
         )
 
-    def test_with_no_config_file_raises(self, no_config_mock):
+    def test_with_no_config_file_raises(self, unused_path):
         with pytest.raises(exception.FileError) as exc_info:
-            config.check_config_integrity()
+            config.check_config_integrity(config_file=unused_path)
 
-        assert str(_repobee.constants.DEFAULT_CONFIG_FILE) in str(
-            exc_info.value
-        )
+        assert str(unused_path) in str(exc_info.value)
 
     def test_with_invalid_defaults_key_raises(self, empty_config_mock):
         empty_config_mock.write(
diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py
index cec05bb..4b818ce 100644
--- a/tests/unit_tests/test_main.py
+++ b/tests/unit_tests/test_main.py
@@ -1,5 +1,7 @@
 import argparse
-from unittest.mock import MagicMock
+import tempfile
+import pathlib
+from unittest.mock import MagicMock, patch
 from collections import namedtuple
 
 import pytest
@@ -93,10 +95,16 @@ def test_happy_path(
     main.main(sys_args)
 
     handle_args_mock.assert_called_once_with(
-        sys_args[1:], show_all_opts=False, ext_commands=DEFAULT_EXT_COMMANDS
+        sys_args[1:],
+        show_all_opts=False,
+        ext_commands=DEFAULT_EXT_COMMANDS,
+        config_file=_repobee.constants.DEFAULT_CONFIG_FILE,
     )
     dispatch_command_mock.assert_called_once_with(
-        PARSED_ARGS, api_instance_mock, DEFAULT_EXT_COMMANDS
+        PARSED_ARGS,
+        api_instance_mock,
+        _repobee.constants.DEFAULT_CONFIG_FILE,
+        DEFAULT_EXT_COMMANDS,
     )
 
 
@@ -112,7 +120,10 @@ def test_exit_status_1_on_exception_in_parsing(
 
     assert exc_info.value.code == 1
     handle_args_mock.assert_called_once_with(
-        sys_args[1:], show_all_opts=False, ext_commands=DEFAULT_EXT_COMMANDS
+        sys_args[1:],
+        show_all_opts=False,
+        ext_commands=DEFAULT_EXT_COMMANDS,
+        config_file=_repobee.constants.DEFAULT_CONFIG_FILE,
     )
     assert not dispatch_command_mock.called
 
@@ -130,7 +141,10 @@ def test_exit_status_1_on_exception_in_handling_parsed_args(
 
     assert exc_info.value.code == 1
     handle_args_mock.assert_called_once_with(
-        sys_args[1:], show_all_opts=False, ext_commands=DEFAULT_EXT_COMMANDS
+        sys_args[1:],
+        show_all_opts=False,
+        ext_commands=DEFAULT_EXT_COMMANDS,
+        config_file=_repobee.constants.DEFAULT_CONFIG_FILE,
     )
 
 
@@ -146,7 +160,10 @@ def test_plugins_args(
         ["javac", "pylint", _repobee.constants.DEFAULT_PLUGIN]
     )
     handle_args_mock.assert_called_once_with(
-        CLONE_ARGS, show_all_opts=False, ext_commands=[]
+        CLONE_ARGS,
+        show_all_opts=False,
+        ext_commands=[],
+        config_file=_repobee.constants.DEFAULT_CONFIG_FILE,
     )
 
 
@@ -164,7 +181,10 @@ def test_no_plugins_arg(
         [_repobee.constants.DEFAULT_PLUGIN]
     )
     handle_args_mock.assert_called_once_with(
-        CLONE_ARGS, show_all_opts=False, ext_commands=[]
+        CLONE_ARGS,
+        show_all_opts=False,
+        ext_commands=[],
+        config_file=_repobee.constants.DEFAULT_CONFIG_FILE,
     )
 
 
@@ -182,7 +202,10 @@ def test_no_plugins_with_configured_plugins(
         [_repobee.constants.DEFAULT_PLUGIN]
     )
     handle_args_mock.assert_called_once_with(
-        CLONE_ARGS, show_all_opts=False, ext_commands=[]
+        CLONE_ARGS,
+        show_all_opts=False,
+        ext_commands=[],
+        config_file=_repobee.constants.DEFAULT_CONFIG_FILE,
     )
 
 
@@ -197,7 +220,10 @@ def test_configured_plugins_are_loaded(
         [*constants.PLUGINS, _repobee.constants.DEFAULT_PLUGIN]
     )
     handle_args_mock.assert_called_once_with(
-        CLONE_ARGS, show_all_opts=False, ext_commands=[]
+        CLONE_ARGS,
+        show_all_opts=False,
+        ext_commands=[],
+        config_file=_repobee.constants.DEFAULT_CONFIG_FILE,
     )
 
 
@@ -212,7 +238,10 @@ def test_plugin_with_subparser_name(
         ["javac", "clone", _repobee.constants.DEFAULT_PLUGIN]
     )
     handle_args_mock.assert_called_once_with(
-        CLONE_ARGS, show_all_opts=False, ext_commands=[]
+        CLONE_ARGS,
+        show_all_opts=False,
+        ext_commands=[],
+        config_file=_repobee.constants.DEFAULT_CONFIG_FILE,
     )
 
 
@@ -290,6 +319,7 @@ def test_logs_traceback_on_exception_in_dispatch_if_traceback(
         [*CLONE_ARGS, "--traceback"],
         show_all_opts=False,
         ext_commands=DEFAULT_EXT_COMMANDS,
+        config_file=_repobee.constants.DEFAULT_CONFIG_FILE,
     )
 
 
@@ -302,7 +332,10 @@ def test_show_all_opts_correctly_separated(
         raise SystemExit(msg)
 
     parse_preparser_options_mock.return_value = argparse.Namespace(
-        show_all_opts=True, no_plugins=False, plug=None
+        show_all_opts=True,
+        no_plugins=False,
+        plug=None,
+        config_file=_repobee.constants.DEFAULT_CONFIG_FILE,
     )
     handle_args_mock.side_effect = _raise_sysexit
     sys_args = [
@@ -320,6 +353,7 @@ def test_show_all_opts_correctly_separated(
         [mainparser.SETUP_PARSER, "-h"],
         show_all_opts=True,
         ext_commands=DEFAULT_EXT_COMMANDS,
+        config_file=_repobee.constants.DEFAULT_CONFIG_FILE,
     )
     parse_preparser_options_mock.assert_called_once_with(
         [_repobee.cli.preparser.PRE_PARSER_SHOW_ALL_OPTS]
@@ -340,3 +374,19 @@ def test_non_zero_exit_status_on_exception(
         main.main(sys_args)
 
     assert exc_info.value.code == 1
+
+
+def test_show_config_custom_config():
+    config_text = """
+[DEFAULTS]
+user = some-unlikely-user
+""".strip()
+    with tempfile.NamedTemporaryFile() as tmpfile, patch(
+        "_repobee.command.show_config", autospec=True
+    ) as show_config_mock:
+        config_file = pathlib.Path(tmpfile.name)
+        config_file.write_text(config_text)
+        sys_args = ["repobee", "--config-file", tmpfile.name, "show-config"]
+        main.main(sys_args)
+
+    show_config_mock.assert_called_once_with(config_file)