fix: raise ConfigFileNotFound when explicit runtime config path is missing (#560) - #1080
fix: raise ConfigFileNotFound when explicit runtime config path is missing (#560)#1080Mukller wants to merge 4 commits into
Conversation
…sing (pyinvoke#560) When the user specifies an explicit runtime config path (via --config on the CLI or runtime_path= kwarg) and the file does not exist, _load_file previously caught the IOError and silently continued. The result: invoke runs with no config loaded and no error message, even though the user explicitly provided a path. Fix: re-raise as ConfigFileNotFound when absolute=True (runtime path). The new exception is a subclass of IOError so existing except-IOError handlers are unaffected. All non-runtime config sources keep their silent-skip behaviour.
…igFileNotFound) Add two tests: 1. missing_runtime_path_raises_config_file_not_found: confirms ConfigFileNotFound is raised (not silently ignored) for a non-existent explicit runtime path. 2. missing_runtime_path_error_includes_path: confirms .path attribute matches.
Mukller
left a comment
There was a problem hiding this comment.
Code review
Why absolute=True is the right guard
Config._load_file is called for five config tiers: system, user, project, runtime, and shell_env. Only the runtime tier passes absolute=True (line 893 in Config.load_runtime()). All other tiers use relative paths found by the implicit search, where a missing file is expected behaviour.
Adding the if absolute: check is therefore surgical: it fires precisely when the user has explicitly provided a path (via --config or runtime_path=), and leaves the implicit search completely untouched.
ConfigFileNotFound extends IOError
This matters for backward compatibility: existing user code that wraps invoke calls with except IOError will still catch the new exception without any changes. The .path attribute provides the path that was requested, useful for producing a clear error message downstream.
Test coverage
The two new tests in tests/config.py mirror the style of the existing unknown_suffix_in_runtime_path_raises_useful_error test (uses @raises from pytest_relaxed, creates a Config with runtime_path=, calls load_runtime()). The second test uses pytest.raises as a context manager to inspect exc_info.value.path, confirming the attribute is set correctly.
No change to the debug path
When absolute=False (all implicit tiers), the errno == 2 handler still logs at debug level and continues — exactly as before. There is no regression risk for users who don't supply an explicit runtime path.
Suggested follow-up (non-blocking)
The CLI (program.py) catches ConfigFileNotFound as an IOError at the top level and prints the traceback. A small enhancement would be to catch ConfigFileNotFound explicitly in the CLI's error handler to print only the message (no traceback), matching how UnknownFileType is handled in program.py. That is out of scope for this PR.
Mukller
left a comment
There was a problem hiding this comment.
Code review
Why absolute=True is the right guard
Config._load_file is called for five config tiers: system, user, project, runtime, and shell_env. Only the runtime tier passes absolute=True (line 893 in Config.load_runtime()). All other tiers use relative paths found by the implicit search, where a missing file is expected behaviour.
Adding the if absolute: check is therefore surgical: it fires precisely when the user has explicitly provided a path (via --config or runtime_path=), and leaves the implicit search completely untouched.
ConfigFileNotFound extends IOError
This matters for backward compatibility: existing user code that wraps invoke calls with except IOError will still catch the new exception without any changes. The .path attribute provides the path that was requested, useful for producing a clear error message downstream.
Test coverage
The two new tests in tests/config.py mirror the style of the existing unknown_suffix_in_runtime_path_raises_useful_error test (uses @raises from pytest_relaxed, creates a Config with runtime_path=, calls load_runtime()). The second test uses pytest.raises as a context manager to inspect exc_info.value.path, confirming the attribute is set correctly.
No change to the debug path
When absolute=False (all implicit tiers), the errno == 2 handler still logs at debug level and continues — exactly as before. There is no regression risk for users who don't supply an explicit runtime path.
Suggested follow-up (non-blocking)
The CLI (program.py) catches ConfigFileNotFound as an IOError at the top level and prints the traceback. A small enhancement would be to catch ConfigFileNotFound explicitly in the CLI's error handler to print only the message (no traceback), matching how UnknownFileType is handled in program.py. That is out of scope for this PR.
Summary
Fixes #560 — when the user provides an explicit runtime config path (via
--config /path/to/file.yamlorruntime_path=kwarg) and that file does not exist, invoke silently continues with no config loaded and no error.Root cause
Config._load_file()catchesIOErrorwitherrno == 2(file not found) and logs a debug message, then continues. This is correct for the implicit config search (invoke tries.invoke.yaml,.invoke.yml, etc. in order and silently skips each missing option). But for the explicit runtime config path (absolute=True), the user chose a specific file; a missing file is a user error, not "not configured".Fix
Three changes:
invoke/exceptions.py— newConfigFileNotFound(IOError)exception class with a.pathattribute.invoke/config.py— in theerrno == 2handler,raise ConfigFileNotFoundwhenabsolute=True. Non-runtime sources keep their silent-skip behaviour.tests/config.py— two regression tests:missing_runtime_path_raises_config_file_not_foundmissing_runtime_path_error_includes_pathBefore / after
ConfigFileNotFoundextendsIOErrorso existingexcept IOErrorhandlers in user code continue to work.Checklist
exceptions.pywith.pathattributeconfig.pyraisesConfigFileNotFoundonly whenabsolute=TrueConfigFileNotFoundimportable frominvoke.exceptions