Lesson learned from PR #1795 (infer dependencies from setup.cfg)
When using @contextmanager to implement UsethisConfig.set(), the cleanup code after yield does NOT run if an exception is raised inside the with block — UNLESS the code uses try-finally.
Root cause
Python's @contextmanager works by re-raising exceptions at the yield point inside the generator. Without a try-finally wrapper around yield, the code after yield never executes when an exception occurs:
@contextmanager
def set(self, *, quiet=None, ...):
old_quiet = self.quiet
self.quiet = quiet
yield # If exception raised here → code below NEVER runs!
self.quiet = old_quiet # ← NEVER runs on exception without try-finally
The fix is:
@contextmanager
def set(self, *, quiet=None, ...):
old_quiet = self.quiet
self.quiet = quiet
try:
yield
finally:
self.quiet = old_quiet # ← ALWAYS runs, even on exception
How this caused cascading CI failures
- My changes made
get_dep_groups() require SetupCFGManager() to be locked
- Tests without
SetupCFGManager() in their context raised UnexpectedSetupCFGIOError
- This exception propagated through
with usethis_config.set(quiet=True): blocks in production code (_core/tool.py)
- Without
try-finally, quiet=True was NOT restored after the exception
- All subsequent tests produced empty output (since
quiet=True suppresses all print functions)
This caused ~80 cascading test failures across test_console.py, test_base.py, and all _tool/impl/base/ test files.
Fix applied in PR #1795
Added try-finally to UsethisConfig.set() in src/usethis/_config.py (commit bfb2905).
Recommendation
Any @contextmanager that modifies shared state MUST wrap the yield in try-finally to guarantee cleanup runs on exceptions:
@contextmanager
def my_context_manager():
# setup...
try:
yield
finally:
# cleanup (always runs, even on exception)
Lesson learned from PR #1795 (infer dependencies from setup.cfg)
When using
@contextmanagerto implementUsethisConfig.set(), the cleanup code afteryielddoes NOT run if an exception is raised inside thewithblock — UNLESS the code usestry-finally.Root cause
Python's
@contextmanagerworks by re-raising exceptions at theyieldpoint inside the generator. Without atry-finallywrapper aroundyield, the code afteryieldnever executes when an exception occurs:The fix is:
How this caused cascading CI failures
get_dep_groups()requireSetupCFGManager()to be lockedSetupCFGManager()in their context raisedUnexpectedSetupCFGIOErrorwith usethis_config.set(quiet=True):blocks in production code (_core/tool.py)try-finally,quiet=Truewas NOT restored after the exceptionquiet=Truesuppresses all print functions)This caused ~80 cascading test failures across
test_console.py,test_base.py, and all_tool/impl/base/test files.Fix applied in PR #1795
Added
try-finallytoUsethisConfig.set()insrc/usethis/_config.py(commit bfb2905).Recommendation
Any
@contextmanagerthat modifies shared state MUST wrap theyieldintry-finallyto guarantee cleanup runs on exceptions: