fix: handle missing files gracefully in remove_real_and_linked_file (#297) - #337
Open
bhumitschaudhry wants to merge 3 commits into
Open
bhumitschaudhry wants to merge 3 commits into
bhumitschaudhry wants to merge 3 commits into
Conversation
Fixes lyogavin#297 The remove_real_and_linked_file() helper had two bugs that caused crashes when delete_original=True was used during model splitting: 1. targetpath was only assigned inside an if-block, causing UnboundLocalError when the input was a regular file (not a symlink). 2. A FileNotFoundError was not caught, so deleting an already-removed file would crash mid-split, leaving users with partial splits and missing source shards. Changes: - Normalize to_delete via os.fspath() so Path objects compare correctly against str realpath. - Initialize targetpath = None before the conditional. - Catch FileNotFoundError on os.remove() and return gracefully. - Add os.path.exists() guard before removing the symlink target. - Add docstring explaining the function's behavior. - Add tests for regular files, Path inputs, missing files, and (Linux/macOS) symlinks. Co-authored-by: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR addresses crashes when delete_original=True triggers remove_real_and_linked_file() during model splitting, making deletion resilient to Path inputs and missing files, and adding unit coverage for the helper.
Changes:
- Harden
remove_real_and_linked_file()againstPathvsstrcomparisons and missing-file deletions. - Add a unittest module covering regular files,
Pathinputs, missing files, and symlink scenarios. - Add an additional “standalone” test script under
air_llm/tests/.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
air_llm/airllm/utils.py |
Makes deletion helper safer for Path inputs and missing files; adds docstring describing behavior. |
air_llm/tests/test_delete_original.py |
Adds unit tests for regular files, missing files, and symlink behavior. |
air_llm/tests/test_delete_original_standalone.py |
Adds a standalone script/test module duplicating the helper and validating behavior in isolation. |
Suppressed comments (4)
air_llm/tests/test_delete_original.py:81
- This symlink test should be skipped on Windows for the same reason as the other symlink test (symlink creation often fails without elevated privileges).
def test_symlink_with_path_object(self):
"""Symlink removal works with Path input too."""
air_llm/tests/test_delete_original.py:103
- Creating a broken symlink is also Windows-hostile (privilege/dev-mode dependent). Skipping this test on Windows avoids CI failures unrelated to the actual behavior under test.
def test_broken_symlink_does_not_crash(self):
"""Deleting a symlink whose target is already gone should not raise."""
link = os.path.join(self.tmpdir, "broken-link.bin")
air_llm/tests/test_delete_original_standalone.py:112
- Same as above: the skip condition should include macOS (
Darwin) if the intent is Linux/macOS.
@unittest.skipUnless(platform.system() == "Linux", "symlink test requires Linux/macOS")
def test_symlink_with_path_object(self):
air_llm/tests/test_delete_original_standalone.py:123
- Same as above: include macOS (
Darwin) in the skip condition to match the message and intended coverage.
@unittest.skipUnless(platform.system() == "Linux", "symlink test requires Linux/macOS")
def test_broken_symlink_does_not_crash(self):
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+199
to
+203
| """Remove a file, following symlinks to also remove the target if present. | ||
|
|
||
| os.remove(to_delete) | ||
| if (targetpath): | ||
| os.remove(targetpath) | ||
| If *to_delete* is a symlink the resolved target is removed after the link | ||
| itself. For regular files the function simply deletes the file. | ||
|
|
Comment on lines
+62
to
+64
| # --- symlinks -------------------------------------------------------- | ||
|
|
||
| def test_symlink_removes_link_and_target(self): |
Comment on lines
+17
to
+35
| # ── Copy of the fixed function (from airllm/utils.py) ────────────────── | ||
|
|
||
| def remove_real_and_linked_file(to_delete): | ||
| """Remove a file, following symlinks to also remove the target if present.""" | ||
| to_delete = os.fspath(to_delete) | ||
| targetpath = None | ||
|
|
||
| realpath = os.path.realpath(to_delete) | ||
| if realpath != to_delete: | ||
| targetpath = realpath | ||
|
|
||
| try: | ||
| os.remove(to_delete) | ||
| except FileNotFoundError: | ||
| return | ||
|
|
||
| if targetpath is not None and os.path.exists(targetpath): | ||
| os.remove(targetpath) | ||
|
|
| remove_real_and_linked_file(os.path.join(self.tmpdir, name)) | ||
| self.assertEqual(os.listdir(self.tmpdir), []) | ||
|
|
||
| @unittest.skipUnless(platform.system() == "Linux", "symlink test requires Linux/macOS") |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…one test Address Copilot review suggestions on PR lyogavin#337: 1. Use os.path.islink() instead of realpath != to_delete to properly detect symlinks (avoids false positives from path normalization). 2. Wrap target removal in try/except FileNotFoundError for TOCTOU race safety under concurrent deletions. 3. Add cross-snapshot warning to docstring about shared HF cache blobs. 4. Add @skipUnless(platform != Windows) to symlink tests in test_delete_original.py. 5. Move test_delete_original_standalone.py to scripts/ so unittest discovery does not pick up the copied function. 6. Fix macOS skip condition: == Linux -> != Windows. Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 16, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #297
The
remove_real_and_linked_file()helper had two bugs that caused crashes whendelete_original=Truewas used during model splitting:targetpathuninitialized —targetpathwas only assigned inside anifblock, causingUnboundLocalErrorwhen the input was a regular file (not a symlink).FileNotFoundErrorwas not caught, so deleting an already-removed file would crash mid-split, leaving users with partial splits and missing source shards.os.path.realpath()returnsstrbut callers passPathobjects, breaking the equality comparison.Changes
air_llm/airllm/utils.py:to_deleteviaos.fspath()soPathobjects compare correctly againststrrealpathtargetpath = Nonebefore the conditionalFileNotFoundErroronos.remove()and return gracefullyos.path.exists()guard before removing the symlink targetair_llm/tests/test_delete_original.py(new):Root Cause
The original code:
Had three bugs:
targetpathwas undefined whento_deletewas a regular file →UnboundLocalErrorPathobjects compared againststrrealpath → symlink detection broken on Path inputsTest Results
Why this matters
delete_original=Trueis specifically for low-disk environments. A crash after deleting original shards leaves users with a partial split, missing source files, and no clear recovery path except re-downloading very large model artifacts.