Skip to content

Fix spurious stderr traceback when the output dir rename target already exists - #41

Merged
paberr merged 2 commits into
paberr:mainfrom
ynaamane:fix-output-dir-rename-collision
Aug 14, 2026
Merged

Fix spurious stderr traceback when the output dir rename target already exists#41
paberr merged 2 commits into
paberr:mainfrom
ynaamane:fix-output-dir-rename-collision

Conversation

@ynaamane

Copy link
Copy Markdown
Contributor

Summary

_rename_output_dir prints a full, scary-looking stack trace to stderr on every summarize or
resume run whose generated title slug collides with an existing, non-empty output directory,
even though the run itself succeeds. This PR makes that a silent, expected skip instead.

Before / after

Before: reproduced directly: pre-create a non-empty directory at the rename target, then run
summarize/resume against a source directory that would rename into it. Path.rename raises
OSError: Directory not empty (errno 66 on macOS/BSD, ENOTEMPTY on Linux), caught by the existing
except Exception, which logs via logger.warning(..., exc_info=True). With no logging handler
configured, Python's default lastResort handler prints the full traceback to stderr. The run
still completes and the summary still saves at the original (unrenamed) path, but the traceback
reads as a crash.

After: the same collision is detected upfront (new_dir.exists() and any(new_dir.iterdir()))
and the function returns the original directory immediately, without attempting the rename or
logging anything. The diff itself is the source of truth.

Tests executed

$ uv run pytest tests/test_pipeline.py -v -k TestRenameOutputDir
======================= 4 passed, 44 deselected in 0.02s =======================

$ uv run pytest
============================= 260 passed in 15.84s =============================

$ uv run ruff check src/ tests/
All checks passed!

(ruff format --check flags 16 pre-existing files repo-wide, unrelated to this change; verified
present on the unmodified pipeline.py at the same commit before this diff was applied.)

What this PR does not do

  • No _2/_N suffixing for repeated collisions on the same final name: that's a separate,
    larger renaming feature and out of scope here.
  • No change to any other function in pipeline.py (title generation, transcription, summarization
    flow are all untouched).
  • No change to what happens on a genuine collision beyond "keep the original directory": the run
    still succeeds either way; this PR only removes the spurious stderr traceback.

…r collision exists

_rename_output_dir attempts directory.rename(new_dir) unconditionally and
falls back to the original path on any exception. On POSIX, renaming into
an existing non-empty directory raises OSError (Directory not empty) --
which two summarize/resume runs landing on the same title slug will hit.
The except branch already returns the correct path, but it logs via
logger.warning(..., exc_info=True) with no configured handler, so Python's
default lastResort handler prints a full traceback to stderr on every run
that happens to collide -- non-blocking, but reads as a crash.

Add an upfront check: if the rename target already exists and has content,
return the original directory directly, without ever attempting the
rename or logging anything. Narrow the fallback except to OSError (was a
bare Exception, which also swallowed programming errors) and drop its
level to debug, so genuinely unexpected OSErrors (permissions,
cross-device links) stay covered but silent by default too.

4 new tests in TestRenameOutputDir. The two that exercise the actual bug
assert on mechanism, not just return value: Path.rename is never even
attempted on a content-collision, and no WARNING-level log record is
emitted anywhere in _rename_output_dir. Verified against the unmodified
function first (skips-cleanly and unexpected-os-error cases fail there
for exactly this reason: rename is attempted and a WARNING is logged),
then against the fix (green), then reverted the fix and confirmed both
tests fail identically again before restoring it.

Full suite: 260 passed (was 256), ruff check clean.
ynaamane added a commit to ynaamane/clew that referenced this pull request Aug 10, 2026
A6's root cause is no longer a hypothesis: a workflow_dispatch run on the
now-public repo (31394710701) died in 5 seconds with the check-run
annotation 'The job was not started because your account is locked due to
a billing issue', so the lock is account-level and public-repo free
minutes do not bypass it. Billable timing for the run is 0 ms.

New post-flip status section: flip executed by Yanis on 2026-08-10,
rendered-README GIFs verified decoded in a real browser, release v0.13.2
asset anonymously downloadable, leak scan of the five rendered surfaces
clean, description + 8 topics set, profile pin is UI-only (no GraphQL
mutation exists), and upstream PR paberr/ownscribe#41 opened on explicit
go.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread src/ownscribe/pipeline.py Outdated
original if renaming isn't safely possible (e.g. the target already
exists with content, or the directory lives outside a renamable tree)."""
new_dir = directory.parent / f"{directory.name}_{title_slug}"
if new_dir.exists() and any(new_dir.iterdir()):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this check be inside the try too? new_dir.exists() and iterdir() sit outside the except OSError now, so if either raises (e.g. a permission error stat'ing the target), it'd propagate uncaught instead of falling back to the original directory like before.


Generated by Claude Code

Comment thread src/ownscribe/pipeline.py Outdated
except Exception:
logging.getLogger(__name__).warning("Could not rename output directory", exc_info=True)
except OSError:
logging.getLogger(__name__).debug("Could not rename output directory", exc_info=True)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also downgrades logging to debug for any other OSError here, not just the known collision case (the test at test_returns_original_on_unexpected_os_error locks that in for e.g. a cross-device rename failure too). Shouldn't a genuinely unexpected error still warning, and only the known collision case stay silent?


Generated by Claude Code

The collision check ran before the try block, so a file or an unreadable
path at the rename target raised out of _rename_output_dir instead of
falling back to the original directory. Unexpected OSErrors now warn
without a traceback; only the expected collision stays silent.
@paberr

paberr commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Thanks a lot for the work, I implemented a couple of minor fixes on top and will merge it next. :)

@paberr
paberr merged commit 185c9f9 into paberr:main Aug 14, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants