From 7e15a032a71944f396840dcd9051a6b45e4b0486 Mon Sep 17 00:00:00 2001 From: beduldul Date: Sat, 24 Oct 2026 18:13:47 +0700 Subject: [PATCH 1/2] fix: create parent dirs from the real directory of the save file 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. --- telebot/handler_backends.py | 5 +++-- tests/test_handler_backends.py | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/telebot/handler_backends.py b/telebot/handler_backends.py index a15d20765..b7bce07ae 100644 --- a/telebot/handler_backends.py +++ b/telebot/handler_backends.py @@ -99,8 +99,9 @@ def load_handlers(self, filename=None, del_file_after_loading=True): @staticmethod def dump_handlers(handlers, filename, file_mode="wb"): - dirs = filename.rsplit('/', maxsplit=1)[0] - os.makedirs(dirs, exist_ok=True) + dirs = os.path.dirname(filename) + if dirs: + os.makedirs(dirs, exist_ok=True) with open(filename + ".tmp", file_mode) as file: if apihelper.CUSTOM_SERIALIZER is None: diff --git a/tests/test_handler_backends.py b/tests/test_handler_backends.py index 5b569720b..9a980ab1b 100644 --- a/tests/test_handler_backends.py +++ b/tests/test_handler_backends.py @@ -285,3 +285,11 @@ def start(message): telegram_bot.process_new_updates([update_type]) assert update_type.message.text == 'entered start' + + +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') From ad7d2ef23fdb61f15e7285d10ec7cd73e7cc1480 Mon Sep 17 00:00:00 2001 From: beduldul Date: Sat, 24 Oct 2026 18:16:59 +0700 Subject: [PATCH 2/2] fix: guard makedirs in the state pickle storages too 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. --- telebot/asyncio_storage/pickle_storage.py | 5 +++-- telebot/storage/pickle_storage.py | 5 +++-- tests/test_pickle_storage.py | 22 ++++++++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 tests/test_pickle_storage.py diff --git a/telebot/asyncio_storage/pickle_storage.py b/telebot/asyncio_storage/pickle_storage.py index 1a63e6961..27609b8d7 100644 --- a/telebot/asyncio_storage/pickle_storage.py +++ b/telebot/asyncio_storage/pickle_storage.py @@ -74,8 +74,9 @@ def create_dir(self): """ Create directory .save-handlers. """ - dirs, filename = os.path.split(self.file_path) - os.makedirs(dirs, exist_ok=True) + dirs = os.path.dirname(self.file_path) + if dirs: + os.makedirs(dirs, exist_ok=True) if not os.path.isfile(self.file_path): with open(self.file_path, "wb") as file: pickle.dump({}, file) diff --git a/telebot/storage/pickle_storage.py b/telebot/storage/pickle_storage.py index b4c44e8fa..2a0e0e717 100644 --- a/telebot/storage/pickle_storage.py +++ b/telebot/storage/pickle_storage.py @@ -63,8 +63,9 @@ def create_dir(self): """ Create directory .save-handlers. """ - dirs, filename = os.path.split(self.file_path) - os.makedirs(dirs, exist_ok=True) + dirs = os.path.dirname(self.file_path) + if dirs: + os.makedirs(dirs, exist_ok=True) if not os.path.isfile(self.file_path): with open(self.file_path, "wb") as file: pickle.dump({}, file) diff --git a/tests/test_pickle_storage.py b/tests/test_pickle_storage.py new file mode 100644 index 000000000..89237e821 --- /dev/null +++ b/tests/test_pickle_storage.py @@ -0,0 +1,22 @@ +import os + +import pytest + +from telebot.storage import StatePickleStorage + + +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") + + +def test_async_state_pickle_storage_accepts_filename_without_directory(tmp_path, monkeypatch): + pytest.importorskip("aiofiles") + from telebot.asyncio_storage import StatePickleStorage as AsyncStatePickleStorage + + monkeypatch.chdir(tmp_path) + AsyncStatePickleStorage(file_path="states.pkl") + + assert os.path.isfile("states.pkl")