Skip to content

fix: create parent dirs from the real directory of the save file - #2642

Merged
Badiboy merged 2 commits into
eternnoir:masterfrom
beduldul:fix/file-handler-backend-bare-filename
Oct 25, 2026
Merged

Badiboy merged 2 commits into
eternnoir:masterfrom
beduldul:fix/file-handler-backend-bare-filename

Conversation

@beduldul

@beduldul beduldul commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

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():

dirs = filename.rsplit('/', maxsplit=1)[0]
os.makedirs(dirs, exist_ok=True)

For filename="step.save", rsplit returns "step.save", the filename itself, so os.makedirs creates a directory with that name. The pickle goes to step.save.tmp, then the rename fails:

IsADirectoryError: [Errno 21] Is a directory: 'step.save.tmp' -> 'step.save'

and you're left with a step.save/ directory plus a stale step.save.tmp.

StatePickleStorage.create_dir() and its async counterpart have the same problem in a different shape:

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:

FileNotFoundError: [Errno 2] No such file or directory: ''

All three now use os.path.dirname() with a guard, so the call is skipped when there is no directory. os.path.dirname also handles Windows separators, which rsplit('/') did not.

Describe your tests

tests/test_handler_backends.py:

def test_dump_handlers_without_directory(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    FileHandlerBackend.dump_handlers({1: ['handler']}, 'step.save')

    assert os.path.isfile('step.save')
    assert not os.path.isdir('step.save')

tests/test_pickle_storage.py:

def test_state_pickle_storage_accepts_filename_without_directory(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    StatePickleStorage(file_path="states.pkl")

    assert os.path.isfile("states.pkl")

plus the same test against telebot.asyncio_storage.StatePickleStorage, guarded with importorskip("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:

  • I added/edited example on new feature/change (if exists) - there is no example for these APIs
  • My changes won't break backward compatibility
  • I made changes both for sync and async

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 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

@Badiboy

Badiboy commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator

Looks good, thank you.

@Badiboy
Badiboy merged commit 82bde1b into eternnoir:master Oct 25, 2026
7 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.

3 participants