Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions invoke/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
AmbiguousEnvVar,
AuthFailure,
CollectionNotFound,
ConfigFileNotFound,
CommandTimedOut,
Exit,
ParseError,
Expand Down
11 changes: 10 additions & 1 deletion invoke/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
from typing import Any, Dict, Iterator, Optional, Tuple, Type, Union

from .env import Environment
from .exceptions import UnknownFileType, UnpicklableConfigMember
from .exceptions import (
ConfigFileNotFound,
UnknownFileType,
UnpicklableConfigMember,
)
from .runners import Local
from .terminals import WINDOWS
from .util import debug, yaml
Expand Down Expand Up @@ -899,6 +903,11 @@ def _load_file(
# Typically means 'no such file', so just note & skip past.
except IOError as e:
if e.errno == 2:
# Absolute paths (runtime config) were explicitly given by
# the user; a missing file is an error,
# not "not configured".
if absolute:
raise ConfigFileNotFound(str(filepath)) from e
err = "Didn't see any {}, skipping."
debug(err.format(filepath))
else:
Expand Down
22 changes: 22 additions & 0 deletions invoke/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,28 @@ class UnknownFileType(Exception):
pass


class ConfigFileNotFound(IOError):
"""
A runtime config file was explicitly specified but could not be found.

Raised by `Config._load_file` when the user supplies an explicit path
(via ``--config`` on the CLI or ``runtime_path=`` kwarg) and that file
does not exist. Unlike the implicit search for system/user/project config
files — where a missing file simply means "not configured" — a missing
*explicit* runtime config is always a user error.

.. versionadded:: 2.3
"""

def __init__(self, path: str) -> None:
self.path = path
msg = (
"Runtime config file {!r} was not found."
" Check the path for typos."
)
super().__init__(msg.format(path))


class UnpicklableConfigMember(Exception):
"""
A config file contained module objects, which can't be pickled/copied.
Expand Down
2 changes: 2 additions & 0 deletions tests/_support/configs/dedupe.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tasks:
dedupe: true
26 changes: 23 additions & 3 deletions tests/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from invoke.exceptions import (
AmbiguousEnvVar,
UncastableEnvVar,
ConfigFileNotFound,
UnknownFileType,
UnpicklableConfigMember,
)
Expand Down Expand Up @@ -619,6 +620,24 @@ def unknown_suffix_in_runtime_path_raises_useful_error(self):
c = Config(runtime_path=join(CONFIGS_PATH, "screw.ini"))
c.load_runtime()

@raises(ConfigFileNotFound)
def missing_runtime_path_raises_config_file_not_found(self):
# Regression for #560: when the user provides an explicit runtime
# config path that doesn't exist, invoke should raise immediately
# instead of silently continuing with no config loaded.
c = Config(
runtime_path=join(CONFIGS_PATH, "nonexistent_typo.yaml")
)
c.load_runtime()

def missing_runtime_path_error_includes_path(self):
# ConfigFileNotFound.path must match what was supplied.
bad = join(CONFIGS_PATH, "nonexistent_typo.yaml")
c = Config(runtime_path=bad)
with pytest.raises(ConfigFileNotFound) as exc_info:
c.load_runtime()
assert exc_info.value.path == bad

def python_modules_dont_load_special_vars(self):
"Python modules don't load special vars"
# Borrow another test's Python module.
Expand All @@ -639,13 +658,14 @@ def python_modules_except_usefully_on_unpicklable_modules(self):
with pytest.raises(UnpicklableConfigMember, match=expected):
c.load_runtime(merge=False)

@patch("invoke.config.debug")
def nonexistent_files_are_skipped_and_logged(self, mock_debug):
@raises(ConfigFileNotFound)
def missing_explicit_runtime_path_raises(self):
# Regression for #560: explicit runtime paths that don't exist must
# raise ConfigFileNotFound, not silently continue.
c = Config()
c._load_yml = Mock(side_effect=IOError(2, "aw nuts"))
c.set_runtime_path("is-a.yml") # Triggers use of _load_yml
c.load_runtime()
mock_debug.assert_any_call("Didn't see any is-a.yml, skipping.")

@raises(IOError)
def non_missing_file_IOErrors_are_raised(self):
Expand Down