Repository navigation
fix: create parent dirs from the real directory of the save file - #2642
Merged
Badiboy merged 2 commits intoOct 25, 2026
Merged
Conversation
dump_handlers() did:
dirs = filename.rsplit('/', maxsplit=1)[0]
os.makedirs(dirs, exist_ok=True)
For a filename with no directory part, e.g. "step.save", rsplit returns
"step.save" itself, so os.makedirs creates a directory with that name and
the rename to the final file then fails:
IsADirectoryError: [Errno 21] Is a directory: 'step.save.tmp' -> 'step.save'
os.path.dirname returns "" for that input, so the makedirs call is skipped.
It also handles Windows separators, which rsplit('/') did not.
create_dir() had the same unguarded pattern:
dirs, filename = os.path.split(self.file_path)
os.makedirs(dirs, exist_ok=True)
os.path.split("states.pkl") returns ("", "states.pkl"), so makedirs gets an
empty path and raises FileNotFoundError. Both the sync and the async storage
are affected.
Same fix as the handler backend: os.path.dirname plus a guard.
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The localized fixes address all three failure paths, preserve nested-directory behavior, and include focused regression coverage with no unresolved findings.
No open issues 🎉
What changed in this PR
Fixes filename-only save paths in handler persistence and synchronous/asynchronous pickle storage, while preserving parent-directory creation for nested paths.
Changes:
- Uses
os.path.dirname()to identify the parent directory. - Skips directory creation when the filename has no directory component.
- Adds regression tests for all three affected implementations.
| File | Description |
|---|---|
tests/test_pickle_storage.py |
Tests filename-only paths in sync and async storage. |
tests/test_handler_backends.py |
Tests handler persistence with a filename-only path. |
telebot/storage/pickle_storage.py |
Guards parent-directory creation in sync storage. |
telebot/handler_backends.py |
Corrects parent-path extraction and guards directory creation. |
telebot/asyncio_storage/pickle_storage.py |
Applies the same directory guard to async storage. |
Review effort: Balanced
Collaborator
|
Looks good, thank you. |
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.
Description
Three places call
os.makedirs()on a path derived from a user-supplied filename without checking whether there is a directory part.FileHandlerBackend.dump_handlers():For
filename="step.save",rsplitreturns"step.save", the filename itself, soos.makedirscreates a directory with that name. The pickle goes tostep.save.tmp, then the rename fails:and you're left with a
step.save/directory plus a stalestep.save.tmp.StatePickleStorage.create_dir()and its async counterpart have the same problem in a different shape:os.path.split("states.pkl")returns("", "states.pkl"), somakedirsgets an empty path:All three now use
os.path.dirname()with a guard, so the call is skipped when there is no directory.os.path.dirnamealso handles Windows separators, whichrsplit('/')did not.Describe your tests
tests/test_handler_backends.py:tests/test_pickle_storage.py:plus the same test against
telebot.asyncio_storage.StatePickleStorage, guarded withimportorskip("aiofiles")since aiofiles is not in requirements.txt.All three fail on master with the tracebacks above and pass with the change.
pytest tests/-> 300 passed, 72 skipped.Python version: 3.12.3
OS: macOS 27.2
Checklist: