From 1edfa1504ce1297745240511641c40f3f2d44cf4 Mon Sep 17 00:00:00 2001 From: eldbud <76731410+eldbud@users.noreply.github.com> Date: Tue, 5 Oct 2021 20:50:11 +0300 Subject: [PATCH] Handle Filepaths via the Pathlib Module (#2688) --- AUTHORS.rst | 1 + examples/arbitrarycallbackdatabot.py | 2 +- examples/passportbot.py | 4 +- examples/persistentconversationbot.py | 2 +- setup.py | 103 ++++++++++---------- telegram/ext/picklepersistence.py | 111 +++++++++++----------- telegram/files/file.py | 51 +++++----- telegram/files/inputfile.py | 4 +- telegram/request.py | 13 ++- tests/conftest.py | 2 +- tests/test_animation.py | 8 +- tests/test_audio.py | 10 +- tests/test_bot.py | 4 +- tests/test_chatphoto.py | 6 +- tests/test_constants.py | 4 +- tests/test_document.py | 6 +- tests/test_file.py | 44 ++++----- tests/test_files.py | 16 ++-- tests/test_inputfile.py | 15 ++- tests/test_inputmedia.py | 17 ++-- tests/test_persistence.py | 131 ++++++++++++++------------ tests/test_photo.py | 16 ++-- tests/test_request.py | 15 +++ tests/test_sticker.py | 31 +++--- tests/test_video.py | 6 +- tests/test_videonote.py | 4 +- tests/test_voice.py | 8 +- 27 files changed, 328 insertions(+), 306 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index cde16caa0..c947fd9f4 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -43,6 +43,7 @@ The following wonderful people contributed directly or indirectly to this projec - `DonalDuck004 `_ - `Eana Hufwe `_ - `Ehsan Online `_ +- `Eldad Carin `_ - `Eli Gao `_ - `Emilio Molinari `_ - `ErgoZ Riftbit Vaper `_ diff --git a/examples/arbitrarycallbackdatabot.py b/examples/arbitrarycallbackdatabot.py index 5ffafb668..4615a6e52 100644 --- a/examples/arbitrarycallbackdatabot.py +++ b/examples/arbitrarycallbackdatabot.py @@ -84,7 +84,7 @@ def handle_invalid_button(update: Update, context: CallbackContext) -> None: def main() -> None: """Run the bot.""" # We use persistence to demonstrate how buttons can still work after the bot was restarted - persistence = PicklePersistence(filename='arbitrarycallbackdatabot.pickle') + persistence = PicklePersistence(filepath='arbitrarycallbackdatabot') # Create the Updater and pass it your bot's token. updater = Updater("TOKEN", persistence=persistence, arbitrary_callback_data=True) diff --git a/examples/passportbot.py b/examples/passportbot.py index dc563e90b..21bfc1ecd 100644 --- a/examples/passportbot.py +++ b/examples/passportbot.py @@ -11,6 +11,7 @@ See https://git.io/fAvYd for how to use Telegram Passport properly with python-t """ import logging +from pathlib import Path from telegram import Update from telegram.ext import Updater, MessageHandler, Filters, CallbackContext @@ -101,8 +102,7 @@ def msg(update: Update, context: CallbackContext) -> None: def main() -> None: """Start the bot.""" # Create the Updater and pass it your token and private key - with open('private.key', 'rb') as private_key: - updater = Updater("TOKEN", private_key=private_key.read()) + updater = Updater("TOKEN", private_key=Path('private.key').read_bytes()) # Get the dispatcher to register handlers dispatcher = updater.dispatcher diff --git a/examples/persistentconversationbot.py b/examples/persistentconversationbot.py index 4a156acfb..e9a2cc47a 100644 --- a/examples/persistentconversationbot.py +++ b/examples/persistentconversationbot.py @@ -132,7 +132,7 @@ def done(update: Update, context: CallbackContext) -> int: def main() -> None: """Run the bot.""" # Create the Updater and pass it your bot's token. - persistence = PicklePersistence(filename='conversationbot') + persistence = PicklePersistence(filepath='conversationbot') updater = Updater("TOKEN", persistence=persistence) # Get the dispatcher to register handlers diff --git a/setup.py b/setup.py index 63a786a32..cce41c4cd 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """The setup and build script for the python-telegram-bot library.""" -import os import subprocess import sys +from pathlib import Path from setuptools import setup, find_packages @@ -13,7 +13,7 @@ def get_requirements(raw=False): """Build the requirements list for this project""" requirements_list = [] - with open('requirements.txt') as reqs: + with Path('requirements.txt').open() as reqs: for install in reqs: if install.startswith('# only telegram.ext:'): if raw: @@ -47,63 +47,60 @@ def get_setup_kwargs(raw=False): packages, requirements = get_packages_requirements(raw=raw) raw_ext = "-raw" if raw else "" - readme = f'README{"_RAW" if raw else ""}.rst' + readme = Path(f'README{"_RAW" if raw else ""}.rst') - fn = os.path.join('telegram', 'version.py') - with open(fn) as fh: + with Path('telegram/version.py').open() as fh: for line in fh.readlines(): if line.startswith('__version__'): exec(line) - with open(readme, 'r', encoding='utf-8') as fd: + kwargs = dict( + script_name=f'setup{raw_ext}.py', + name=f'python-telegram-bot{raw_ext}', + version=locals()['__version__'], + author='Leandro Toledo', + author_email='devs@python-telegram-bot.org', + license='LGPLv3', + url='https://python-telegram-bot.org/', + # Keywords supported by PyPI can be found at https://git.io/JtLIZ + project_urls={ + "Documentation": "https://python-telegram-bot.readthedocs.io", + "Bug Tracker": "https://github.com/python-telegram-bot/python-telegram-bot/issues", + "Source Code": "https://github.com/python-telegram-bot/python-telegram-bot", + "News": "https://t.me/pythontelegrambotchannel", + "Changelog": "https://python-telegram-bot.readthedocs.io/en/stable/changelog.html", + }, + download_url=f'https://pypi.org/project/python-telegram-bot{raw_ext}/', + keywords='python telegram bot api wrapper', + description="We have made you a wrapper you can't refuse", + long_description=readme.read_text(), + long_description_content_type='text/x-rst', + packages=packages, - kwargs = dict( - script_name=f'setup{raw_ext}.py', - name=f'python-telegram-bot{raw_ext}', - version=locals()['__version__'], - author='Leandro Toledo', - author_email='devs@python-telegram-bot.org', - license='LGPLv3', - url='https://python-telegram-bot.org/', - # Keywords supported by PyPI can be found at https://git.io/JtLIZ - project_urls={ - "Documentation": "https://python-telegram-bot.readthedocs.io", - "Bug Tracker": "https://github.com/python-telegram-bot/python-telegram-bot/issues", - "Source Code": "https://github.com/python-telegram-bot/python-telegram-bot", - "News": "https://t.me/pythontelegrambotchannel", - "Changelog": "https://python-telegram-bot.readthedocs.io/en/stable/changelog.html", - }, - download_url=f'https://pypi.org/project/python-telegram-bot{raw_ext}/', - keywords='python telegram bot api wrapper', - description="We have made you a wrapper you can't refuse", - long_description=fd.read(), - long_description_content_type='text/x-rst', - packages=packages, - - install_requires=requirements, - extras_require={ - 'json': 'ujson', - 'socks': 'PySocks', - # 3.4-3.4.3 contained some cyclical import bugs - 'passport': 'cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3', - }, - include_package_data=True, - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', - 'Operating System :: OS Independent', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Communications :: Chat', - 'Topic :: Internet', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - ], - python_requires='>=3.7' - ) + install_requires=requirements, + extras_require={ + 'json': 'ujson', + 'socks': 'PySocks', + # 3.4-3.4.3 contained some cyclical import bugs + 'passport': 'cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3', + }, + include_package_data=True, + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', + 'Operating System :: OS Independent', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Communications :: Chat', + 'Topic :: Internet', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + ], + python_requires='>=3.7' + ) return kwargs diff --git a/telegram/ext/picklepersistence.py b/telegram/ext/picklepersistence.py index fce63bb2a..2beb3af0d 100644 --- a/telegram/ext/picklepersistence.py +++ b/telegram/ext/picklepersistence.py @@ -19,6 +19,7 @@ """This module contains the PicklePersistence class.""" import pickle from collections import defaultdict +from pathlib import Path from typing import ( Any, Dict, @@ -27,6 +28,7 @@ from typing import ( overload, cast, DefaultDict, + Union, ) from telegram.ext import BasePersistence, PersistenceInput @@ -47,11 +49,14 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): :meth:`telegram.ext.BasePersistence.insert_bot`. .. versionchanged:: 14.0 - The parameters and attributes ``store_*_data`` were replaced by :attr:`store_data`. + * The parameters and attributes ``store_*_data`` were replaced by :attr:`store_data`. + * The parameter and attribute ``filename`` were replaced by :attr:`filepath`. + * :attr:`filepath` now also accepts :obj:`pathlib.Path` as argument. + Args: - filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file` - is :obj:`False` this will be used as a prefix. + filepath (:obj:`str` | :obj:`pathlib.Path`): The filepath for storing the pickle files. + When :attr:`single_file` is :obj:`False` this will be used as a prefix. store_data (:class:`PersistenceInput`, optional): Specifies which kinds of data will be saved by this persistence instance. By default, all available kinds of data will be saved. @@ -70,8 +75,8 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): .. versionadded:: 13.6 Attributes: - filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file` - is :obj:`False` this will be used as a prefix. + filepath (:obj:`str` | :obj:`pathlib.Path`): The filepath for storing the pickle files. + When :attr:`single_file` is :obj:`False` this will be used as a prefix. store_data (:class:`PersistenceInput`): Specifies which kinds of data will be saved by this persistence instance. single_file (:obj:`bool`): Optional. When :obj:`False` will store 5 separate files of @@ -88,7 +93,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): """ __slots__ = ( - 'filename', + 'filepath', 'single_file', 'on_flush', 'user_data', @@ -102,7 +107,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): @overload def __init__( self: 'PicklePersistence[Dict, Dict, Dict]', - filename: str, + filepath: Union[Path, str], store_data: PersistenceInput = None, single_file: bool = True, on_flush: bool = False, @@ -112,7 +117,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): @overload def __init__( self: 'PicklePersistence[UD, CD, BD]', - filename: str, + filepath: Union[Path, str], store_data: PersistenceInput = None, single_file: bool = True, on_flush: bool = False, @@ -122,14 +127,14 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): def __init__( self, - filename: str, + filepath: Union[Path, str], store_data: PersistenceInput = None, single_file: bool = True, on_flush: bool = False, context_types: ContextTypes[Any, UD, CD, BD] = None, ): super().__init__(store_data=store_data) - self.filename = filename + self.filepath = Path(filepath) self.single_file = single_file self.on_flush = on_flush self.user_data: Optional[DefaultDict[int, UD]] = None @@ -141,15 +146,14 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): def _load_singlefile(self) -> None: try: - filename = self.filename - with open(self.filename, "rb") as file: + with self.filepath.open("rb") as file: data = pickle.load(file) - self.user_data = defaultdict(self.context_types.user_data, data['user_data']) - self.chat_data = defaultdict(self.context_types.chat_data, data['chat_data']) - # For backwards compatibility with files not containing bot data - self.bot_data = data.get('bot_data', self.context_types.bot_data()) - self.callback_data = data.get('callback_data', {}) - self.conversations = data['conversations'] + self.user_data = defaultdict(self.context_types.user_data, data['user_data']) + self.chat_data = defaultdict(self.context_types.chat_data, data['chat_data']) + # For backwards compatibility with files not containing bot data + self.bot_data = data.get('bot_data', self.context_types.bot_data()) + self.callback_data = data.get('callback_data', {}) + self.conversations = data['conversations'] except OSError: self.conversations = {} self.user_data = defaultdict(self.context_types.user_data) @@ -157,36 +161,37 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): self.bot_data = self.context_types.bot_data() self.callback_data = None except pickle.UnpicklingError as exc: + filename = self.filepath.name raise TypeError(f"File {filename} does not contain valid pickle data") from exc except Exception as exc: - raise TypeError(f"Something went wrong unpickling {filename}") from exc + raise TypeError(f"Something went wrong unpickling {self.filepath.name}") from exc @staticmethod - def _load_file(filename: str) -> Any: + def _load_file(filepath: Path) -> Any: try: - with open(filename, "rb") as file: + with filepath.open("rb") as file: return pickle.load(file) except OSError: return None except pickle.UnpicklingError as exc: - raise TypeError(f"File {filename} does not contain valid pickle data") from exc + raise TypeError(f"File {filepath.name} does not contain valid pickle data") from exc except Exception as exc: - raise TypeError(f"Something went wrong unpickling {filename}") from exc + raise TypeError(f"Something went wrong unpickling {filepath.name}") from exc def _dump_singlefile(self) -> None: - with open(self.filename, "wb") as file: - data = { - 'conversations': self.conversations, - 'user_data': self.user_data, - 'chat_data': self.chat_data, - 'bot_data': self.bot_data, - 'callback_data': self.callback_data, - } + data = { + 'conversations': self.conversations, + 'user_data': self.user_data, + 'chat_data': self.chat_data, + 'bot_data': self.bot_data, + 'callback_data': self.callback_data, + } + with self.filepath.open("wb") as file: pickle.dump(data, file) @staticmethod - def _dump_file(filename: str, data: object) -> None: - with open(filename, "wb") as file: + def _dump_file(filepath: Path, data: object) -> None: + with filepath.open("wb") as file: pickle.dump(data, file) def get_user_data(self) -> DefaultDict[int, UD]: @@ -198,8 +203,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): if self.user_data: pass elif not self.single_file: - filename = f"{self.filename}_user_data" - data = self._load_file(filename) + data = self._load_file(Path(f"{self.filepath}_user_data")) if not data: data = defaultdict(self.context_types.user_data) else: @@ -218,8 +222,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): if self.chat_data: pass elif not self.single_file: - filename = f"{self.filename}_chat_data" - data = self._load_file(filename) + data = self._load_file(Path(f"{self.filepath}_chat_data")) if not data: data = defaultdict(self.context_types.chat_data) else: @@ -239,8 +242,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): if self.bot_data: pass elif not self.single_file: - filename = f"{self.filename}_bot_data" - data = self._load_file(filename) + data = self._load_file(Path(f"{self.filepath}_bot_data")) if not data: data = self.context_types.bot_data() self.bot_data = data @@ -260,8 +262,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): if self.callback_data: pass elif not self.single_file: - filename = f"{self.filename}_callback_data" - data = self._load_file(filename) + data = self._load_file(Path(f"{self.filepath}_callback_data")) if not data: data = None self.callback_data = data @@ -283,8 +284,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): if self.conversations: pass elif not self.single_file: - filename = f"{self.filename}_conversations" - data = self._load_file(filename) + data = self._load_file(Path(f"{self.filepath}_conversations")) if not data: data = {name: {}} self.conversations = data @@ -310,8 +310,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): self.conversations[name][key] = new_state if not self.on_flush: if not self.single_file: - filename = f"{self.filename}_conversations" - self._dump_file(filename, self.conversations) + self._dump_file(Path(f"{self.filepath}_conversations"), self.conversations) else: self._dump_singlefile() @@ -330,8 +329,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): self.user_data[user_id] = data if not self.on_flush: if not self.single_file: - filename = f"{self.filename}_user_data" - self._dump_file(filename, self.user_data) + self._dump_file(Path(f"{self.filepath}_user_data"), self.user_data) else: self._dump_singlefile() @@ -350,8 +348,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): self.chat_data[chat_id] = data if not self.on_flush: if not self.single_file: - filename = f"{self.filename}_chat_data" - self._dump_file(filename, self.chat_data) + self._dump_file(Path(f"{self.filepath}_chat_data"), self.chat_data) else: self._dump_singlefile() @@ -367,8 +364,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): self.bot_data = data if not self.on_flush: if not self.single_file: - filename = f"{self.filename}_bot_data" - self._dump_file(filename, self.bot_data) + self._dump_file(Path(f"{self.filepath}_bot_data"), self.bot_data) else: self._dump_singlefile() @@ -387,8 +383,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): self.callback_data = (data[0], data[1].copy()) if not self.on_flush: if not self.single_file: - filename = f"{self.filename}_callback_data" - self._dump_file(filename, self.callback_data) + self._dump_file(Path(f"{self.filepath}_callback_data"), self.callback_data) else: self._dump_singlefile() @@ -426,12 +421,12 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): self._dump_singlefile() else: if self.user_data: - self._dump_file(f"{self.filename}_user_data", self.user_data) + self._dump_file(Path(f"{self.filepath}_user_data"), self.user_data) if self.chat_data: - self._dump_file(f"{self.filename}_chat_data", self.chat_data) + self._dump_file(Path(f"{self.filepath}_chat_data"), self.chat_data) if self.bot_data: - self._dump_file(f"{self.filename}_bot_data", self.bot_data) + self._dump_file(Path(f"{self.filepath}_bot_data"), self.bot_data) if self.callback_data: - self._dump_file(f"{self.filename}_callback_data", self.callback_data) + self._dump_file(Path(f"{self.filepath}_callback_data"), self.callback_data) if self.conversations: - self._dump_file(f"{self.filename}_conversations", self.conversations) + self._dump_file(Path(f"{self.filepath}_conversations"), self.conversations) diff --git a/telegram/files/file.py b/telegram/files/file.py index eac51e1ed..75c917e26 100644 --- a/telegram/files/file.py +++ b/telegram/files/file.py @@ -17,11 +17,10 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram File.""" -import os import shutil import urllib.parse as urllib_parse from base64 import b64decode -from os.path import basename +from pathlib import Path from typing import IO, TYPE_CHECKING, Any, Optional, Union from telegram import TelegramObject @@ -97,8 +96,8 @@ class File(TelegramObject): self._id_attrs = (self.file_unique_id,) def download( - self, custom_path: str = None, out: IO = None, timeout: int = None - ) -> Union[str, IO]: + self, custom_path: Union[Path, str] = None, out: IO = None, timeout: int = None + ) -> Union[Path, IO]: """ Download this file. By default, the file is saved in the current working directory with its original filename as reported by Telegram. If the file has no filename, it the file ID will @@ -112,8 +111,12 @@ class File(TelegramObject): the path of a local file (which is the case when a Bot API Server is running in local mode), this method will just return the path. + .. versionchanged:: 14.0 + * ``custom_path`` parameter now also accepts :obj:`pathlib.Path` as argument. + * Returns :obj:`pathlib.Path` object in cases where previously returned `str` object. + Args: - custom_path (:obj:`str`, optional): Custom path. + custom_path (:obj:`pathlib.Path` | :obj:`str`, optional): Custom path. out (:obj:`io.BufferedWriter`, optional): A file-like object. Must be opened for writing in binary mode, if applicable. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as @@ -121,7 +124,8 @@ class File(TelegramObject): the connection pool). Returns: - :obj:`str` | :obj:`io.BufferedWriter`: The same object as :attr:`out` if specified. + :obj:`pathlib.Path` | :obj:`io.BufferedWriter`: The same object as :attr:`out` if + specified. Otherwise, returns the filename downloaded to or the file path of the local file. Raises: @@ -129,20 +133,15 @@ class File(TelegramObject): """ if custom_path is not None and out is not None: - raise ValueError('custom_path and out are mutually exclusive') + raise ValueError('`custom_path` and `out` are mutually exclusive') local_file = is_local_file(self.file_path) - - if local_file: - url = self.file_path - else: - # Convert any UTF-8 char into a url encoded ASCII string. - url = self._get_encoded_url() + url = None if local_file else self._get_encoded_url() + path = Path(self.file_path) if local_file else None if out: if local_file: - with open(url, 'rb') as file: - buf = file.read() + buf = path.read_bytes() else: buf = self.bot.request.retrieve(url) if self._credentials: @@ -152,31 +151,30 @@ class File(TelegramObject): out.write(buf) return out - if custom_path and local_file: - shutil.copyfile(self.file_path, custom_path) - return custom_path + if custom_path is not None and local_file: + shutil.copyfile(self.file_path, str(custom_path)) + return Path(custom_path) if custom_path: - filename = custom_path + filename = Path(custom_path) elif local_file: - return self.file_path + return Path(self.file_path) elif self.file_path: - filename = basename(self.file_path) + filename = Path(Path(self.file_path).name) else: - filename = os.path.join(os.getcwd(), self.file_id) + filename = Path.cwd() / self.file_id buf = self.bot.request.retrieve(url, timeout=timeout) if self._credentials: buf = decrypt( b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf ) - with open(filename, 'wb') as fobj: - fobj.write(buf) + filename.write_bytes(buf) return filename def _get_encoded_url(self) -> str: """Convert any UTF-8 char in :obj:`File.file_path` into a url encoded ASCII string.""" - sres = urllib_parse.urlsplit(self.file_path) + sres = urllib_parse.urlsplit(str(self.file_path)) return urllib_parse.urlunsplit( urllib_parse.SplitResult( sres.scheme, sres.netloc, urllib_parse.quote(sres.path), sres.query, sres.fragment @@ -197,8 +195,7 @@ class File(TelegramObject): if buf is None: buf = bytearray() if is_local_file(self.file_path): - with open(self.file_path, "rb") as file: - buf.extend(file.read()) + buf.extend(Path(self.file_path).read_bytes()) else: buf.extend(self.bot.request.retrieve(self._get_encoded_url())) return buf diff --git a/telegram/files/inputfile.py b/telegram/files/inputfile.py index d76f2a8bc..16dcd118a 100644 --- a/telegram/files/inputfile.py +++ b/telegram/files/inputfile.py @@ -22,7 +22,7 @@ import imghdr import logging import mimetypes -import os +from pathlib import Path from typing import IO, Optional, Tuple, Union from uuid import uuid4 @@ -64,7 +64,7 @@ class InputFile: if filename: self.filename = filename elif hasattr(obj, 'name') and not isinstance(obj.name, int): # type: ignore[union-attr] - self.filename = os.path.basename(obj.name) # type: ignore[union-attr] + self.filename = Path(obj.name).name # type: ignore[union-attr] image_mime_type = self.is_image(self.input_file_content) if image_mime_type: diff --git a/telegram/request.py b/telegram/request.py index fa046cd33..2c5375012 100644 --- a/telegram/request.py +++ b/telegram/request.py @@ -24,6 +24,7 @@ import os import socket import sys import warnings +from pathlib import Path try: import ujson as json @@ -80,6 +81,7 @@ def _render_part(self: RequestField, name: str, value: str) -> str: # pylint: d Monkey patch urllib3.urllib3.fields.RequestField to make it *not* support RFC2231 compliant Content-Disposition headers since telegram servers don't understand it. Instead just escape \\ and " and replace any \n and \r with a space. + """ value = value.replace('\\', '\\\\').replace('"', '\\"') value = value.replace('\r', ' ').replace('\n', ' ') @@ -382,17 +384,18 @@ class Request: return self._request_wrapper('GET', url, **urlopen_kwargs) - def download(self, url: str, filename: str, timeout: float = None) -> None: + def download(self, url: str, filepath: Union[Path, str], timeout: float = None) -> None: """Download a file by its URL. Args: url (:obj:`str`): The web location we want to retrieve. + filepath (:obj:`pathlib.Path` | :obj:`str`): The filepath to download the file to. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - filename (:obj:`str`): The filename within the path to download the file. + + .. versionchanged:: 14.0 + The ``filepath`` parameter now also accepts :obj:`pathlib.Path` objects as argument. """ - buf = self.retrieve(url, timeout=timeout) - with open(filename, 'wb') as fobj: - fobj.write(buf) + Path(filepath).write_bytes(self.retrieve(url, timeout)) diff --git a/tests/conftest.py b/tests/conftest.py index b1d9a1047..094b8e041 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -120,7 +120,7 @@ def bot(bot_info): @pytest.fixture(scope='session') def raw_bot(bot_info): - return DictBot(bot_info['token'], private_key=PRIVATE_KEY, request=DictRequest()) + return DictBot(bot_info['token'], private_key=PRIVATE_KEY, request=DictRequest(8)) DEFAULT_BOTS = {} diff --git a/tests/test_animation.py b/tests/test_animation.py index 5cc9028e0..89d3136c9 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -30,14 +30,14 @@ from tests.conftest import check_shortcut_call, check_shortcut_signature, check_ @pytest.fixture(scope='function') def animation_file(): - f = open('tests/data/game.gif', 'rb') + f = Path('tests/data/game.gif').open('rb') yield f f.close() @pytest.fixture(scope='class') def animation(bot, chat_id): - with open('tests/data/game.gif', 'rb') as f: + with Path('tests/data/game.gif').open('rb') as f: return bot.send_animation( chat_id, animation=f, timeout=50, thumb=open('tests/data/thumb.jpg', 'rb') ).animation @@ -120,9 +120,9 @@ class TestAnimation: assert new_file.file_id == animation.file_id assert new_file.file_path.startswith('https://') - new_file.download('game.gif') + new_filepath: Path = new_file.download('game.gif') - assert os.path.isfile('game.gif') + assert new_filepath.is_file() @flaky(3, 1) def test_send_animation_url_file(self, bot, chat_id, animation): diff --git a/tests/test_audio.py b/tests/test_audio.py index e4dcd568f..5c55c722e 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -30,16 +30,16 @@ from tests.conftest import check_shortcut_call, check_shortcut_signature, check_ @pytest.fixture(scope='function') def audio_file(): - f = open('tests/data/telegram.mp3', 'rb') + f = Path('tests/data/telegram.mp3').open('rb') yield f f.close() @pytest.fixture(scope='class') def audio(bot, chat_id): - with open('tests/data/telegram.mp3', 'rb') as f: + with Path('tests/data/telegram.mp3').open('rb') as f: return bot.send_audio( - chat_id, audio=f, timeout=50, thumb=open('tests/data/thumb.jpg', 'rb') + chat_id, audio=f, timeout=50, thumb=Path('tests/data/thumb.jpg').open('rb') ).audio @@ -132,11 +132,11 @@ class TestAudio: assert new_file.file_size == self.file_size assert new_file.file_id == audio.file_id assert new_file.file_unique_id == audio.file_unique_id - assert new_file.file_path.startswith('https://') + assert str(new_file.file_path).startswith('https://') new_file.download('telegram.mp3') - assert os.path.isfile('telegram.mp3') + assert Path('telegram.mp3').is_file() @flaky(3, 1) def test_send_mp3_url_file(self, bot, chat_id, audio): diff --git a/tests/test_bot.py b/tests/test_bot.py index 88651d687..ef5c20b88 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -100,7 +100,7 @@ def message(bot, chat_id): @pytest.fixture(scope='class') def media_message(bot, chat_id): - with open('tests/data/telegram.ogg', 'rb') as f: + with Path('tests/data/telegram.ogg').open('rb') as f: return bot.send_voice(chat_id, voice=f, caption='my caption', timeout=10) @@ -1925,7 +1925,7 @@ class TestBot: def func(): assert bot.set_chat_photo(channel_id, f) - with open('tests/data/telegram_test_channel.jpg', 'rb') as f: + with Path('tests/data/telegram_test_channel.jpg').open('rb') as f: expect_bad_request(func, 'Type of file mismatch', 'Telegram did not accept the file.') def test_set_chat_photo_local_files(self, monkeypatch, bot, chat_id): diff --git a/tests/test_chatphoto.py b/tests/test_chatphoto.py index b67762e49..f76445ee4 100644 --- a/tests/test_chatphoto.py +++ b/tests/test_chatphoto.py @@ -17,6 +17,8 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import os +from pathlib import Path + import pytest from flaky import flaky @@ -73,7 +75,7 @@ class TestChatPhoto: new_file.download('telegram.jpg') - assert os.path.isfile('telegram.jpg') + assert Path('telegram.jpg').is_file() new_file = bot.get_file(chat_photo.big_file_id) @@ -82,7 +84,7 @@ class TestChatPhoto: new_file.download('telegram.jpg') - assert os.path.isfile('telegram.jpg') + assert Path('telegram.jpg').is_file() def test_send_with_chat_photo(self, monkeypatch, bot, super_group_id, chat_photo): def test(url, data, **kwargs): diff --git a/tests/test_constants.py b/tests/test_constants.py index 96884249f..17569a4f2 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -16,6 +16,8 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +from pathlib import Path + import pytest from flaky import flaky @@ -37,7 +39,7 @@ class TestConstants: @flaky(3, 1) def test_max_caption_length(self, bot, chat_id): good_caption = 'a' * constants.MAX_CAPTION_LENGTH - with open('tests/data/telegram.png', 'rb') as f: + with Path('tests/data/telegram.png').open('rb') as f: good_msg = bot.send_photo(photo=f, caption=good_caption, chat_id=chat_id) assert good_msg.caption == good_caption diff --git a/tests/test_document.py b/tests/test_document.py index 9dae00b52..5474af603 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -37,7 +37,7 @@ def document_file(): @pytest.fixture(scope='class') def document(bot, chat_id): - with open('tests/data/telegram.png', 'rb') as f: + with Path('tests/data/telegram.png').open('rb') as f: return bot.send_document(chat_id, document=f, timeout=50).document @@ -111,7 +111,7 @@ class TestDocument: new_file.download('telegram.png') - assert os.path.isfile('telegram.png') + assert Path('telegram.png').is_file() @flaky(3, 1) def test_send_url_gif_file(self, bot, chat_id): @@ -281,7 +281,7 @@ class TestDocument: @flaky(3, 1) def test_error_send_empty_file(self, bot, chat_id): - with open(os.devnull, 'rb') as f, pytest.raises(TelegramError): + with Path(os.devnull).open('rb') as f, pytest.raises(TelegramError): bot.send_document(chat_id=chat_id, document=f) @flaky(3, 1) diff --git a/tests/test_file.py b/tests/test_file.py index 70eb71f61..7f12df1e3 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -92,7 +92,7 @@ class TestFile: bot.get_file(file_id='') def test_download_mutuall_exclusive(self, file): - with pytest.raises(ValueError, match='custom_path and out are mutually exclusive'): + with pytest.raises(ValueError, match='`custom_path` and `out` are mutually exclusive'): file.download('custom_path', 'out') def test_download(self, monkeypatch, file): @@ -103,41 +103,44 @@ class TestFile: out_file = file.download() try: - with open(out_file, 'rb') as fobj: - assert fobj.read() == self.file_content + assert out_file.read_bytes() == self.file_content finally: - os.unlink(out_file) + out_file.unlink() def test_download_local_file(self, local_file): - assert local_file.download() == local_file.file_path + assert local_file.download() == Path(local_file.file_path) - def test_download_custom_path(self, monkeypatch, file): + @pytest.mark.parametrize( + 'custom_path_type', [str, Path], ids=['str custom_path', 'pathlib.Path custom_path'] + ) + def test_download_custom_path(self, monkeypatch, file, custom_path_type): def test(*args, **kwargs): return self.file_content monkeypatch.setattr('telegram.request.Request.retrieve', test) file_handle, custom_path = mkstemp() + custom_path = Path(custom_path) try: - out_file = file.download(custom_path) + out_file = file.download(custom_path_type(custom_path)) assert out_file == custom_path - - with open(out_file, 'rb') as fobj: - assert fobj.read() == self.file_content + assert out_file.read_bytes() == self.file_content finally: os.close(file_handle) - os.unlink(custom_path) + custom_path.unlink() - def test_download_custom_path_local_file(self, local_file): + @pytest.mark.parametrize( + 'custom_path_type', [str, Path], ids=['str custom_path', 'pathlib.Path custom_path'] + ) + def test_download_custom_path_local_file(self, local_file, custom_path_type): file_handle, custom_path = mkstemp() + custom_path = Path(custom_path) try: - out_file = local_file.download(custom_path) + out_file = local_file.download(custom_path_type(custom_path)) assert out_file == custom_path - - with open(out_file, 'rb') as fobj: - assert fobj.read() == self.file_content + assert out_file.read_bytes() == self.file_content finally: os.close(file_handle) - os.unlink(custom_path) + custom_path.unlink() def test_download_no_filename(self, monkeypatch, file): def test(*args, **kwargs): @@ -148,12 +151,11 @@ class TestFile: monkeypatch.setattr('telegram.request.Request.retrieve', test) out_file = file.download() - assert out_file[-len(file.file_id) :] == file.file_id + assert str(out_file)[-len(file.file_id) :] == file.file_id try: - with open(out_file, 'rb') as fobj: - assert fobj.read() == self.file_content + assert out_file.read_bytes() == self.file_content finally: - os.unlink(out_file) + out_file.unlink() def test_download_file_obj(self, monkeypatch, file): def test(*args, **kwargs): diff --git a/tests/test_files.py b/tests/test_files.py index 9da4e856c..ed83ec66d 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -68,14 +68,15 @@ class TestFiles: assert telegram.utils.files.parse_file_input(string) == expected def test_parse_file_input_file_like(self): - with open('tests/data/game.gif', 'rb') as file: + source_file = Path('tests/data/game.gif') + with source_file.open('rb') as file: parsed = telegram.utils.files.parse_file_input(file) assert isinstance(parsed, InputFile) assert not parsed.attach assert parsed.filename == 'game.gif' - with open('tests/data/game.gif', 'rb') as file: + with source_file.open('rb') as file: parsed = telegram.utils.files.parse_file_input(file, attach=True, filename='test_file') assert isinstance(parsed, InputFile) @@ -83,17 +84,16 @@ class TestFiles: assert parsed.filename == 'test_file' def test_parse_file_input_bytes(self): - with open('tests/data/text_file.txt', 'rb') as file: - parsed = telegram.utils.files.parse_file_input(file.read()) + source_file = Path('tests/data/text_file.txt') + parsed = telegram.utils.files.parse_file_input(source_file.read_bytes()) assert isinstance(parsed, InputFile) assert not parsed.attach assert parsed.filename == 'application.octet-stream' - with open('tests/data/text_file.txt', 'rb') as file: - parsed = telegram.utils.files.parse_file_input( - file.read(), attach=True, filename='test_file' - ) + parsed = telegram.utils.files.parse_file_input( + source_file.read_bytes(), attach=True, filename='test_file' + ) assert isinstance(parsed, InputFile) assert parsed.attach diff --git a/tests/test_inputfile.py b/tests/test_inputfile.py index c246ef449..2f1e50487 100644 --- a/tests/test_inputfile.py +++ b/tests/test_inputfile.py @@ -17,16 +17,16 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import logging -import os import subprocess import sys from io import BytesIO +from pathlib import Path from telegram import InputFile class TestInputFile: - png = os.path.join('tests', 'data', 'game.png') + png = Path('tests/data/game.png') def test_slot_behaviour(self, mro_slots): inst = InputFile(BytesIO(b'blah'), filename='tg.jpg') @@ -35,15 +35,12 @@ class TestInputFile: assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" def test_subprocess_pipe(self): - if sys.platform == 'win32': - cmd = ['type', self.png] - else: - cmd = ['cat', self.png] - + cmd_str = 'type' if sys.platform == 'win32' else 'cat' + cmd = [cmd_str, str(self.png)] proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=(sys.platform == 'win32')) in_file = InputFile(proc.stdout) - assert in_file.input_file_content == open(self.png, 'rb').read() + assert in_file.input_file_content == self.png.read_bytes() assert in_file.mimetype == 'image/png' assert in_file.filename == 'image.png' @@ -124,7 +121,7 @@ class TestInputFile: def test_send_bytes(self, bot, chat_id): # We test this here and not at the respective test modules because it's not worth # duplicating the test for the different methods - with open('tests/data/text_file.txt', 'rb') as file: + with Path('tests/data/text_file.txt').open('rb') as file: message = bot.send_document(chat_id, file.read()) out = BytesIO() diff --git a/tests/test_inputmedia.py b/tests/test_inputmedia.py index d9cb3993a..378a6b260 100644 --- a/tests/test_inputmedia.py +++ b/tests/test_inputmedia.py @@ -510,15 +510,14 @@ class TestSendMediaGroup: self, bot, chat_id, video_file, photo_file, animation_file # noqa: F811 ): # noqa: F811 def func(): - with open('tests/data/telegram.jpg', 'rb') as file: - return bot.send_media_group( - chat_id, - [ - InputMediaVideo(video_file), - InputMediaPhoto(photo_file), - InputMediaPhoto(file.read()), - ], - ) + return bot.send_media_group( + chat_id, + [ + InputMediaVideo(video_file), + InputMediaPhoto(photo_file), + InputMediaPhoto(Path('tests/data/telegram.jpg').read_bytes()), + ], + ) messages = expect_bad_request( func, 'Type of file mismatch', 'Telegram did not accept the file.' diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 5ad477229..6e0dbaba9 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -19,6 +19,7 @@ import gzip import signal import uuid +from pathlib import Path from threading import Lock from telegram.ext import PersistenceInput @@ -55,7 +56,7 @@ from telegram.ext import ( @pytest.fixture(autouse=True) def change_directory(tmp_path): - orig_dir = os.getcwd() + orig_dir = Path.cwd() # Switch to a temporary directory so we don't have to worry about cleaning up files # (str() for py<3.6) os.chdir(str(tmp_path)) @@ -871,7 +872,7 @@ class TestBasePersistence: @pytest.fixture(scope='function') def pickle_persistence(): return PicklePersistence( - filename='pickletest', + filepath='pickletest', single_file=False, on_flush=False, ) @@ -880,7 +881,7 @@ def pickle_persistence(): @pytest.fixture(scope='function') def pickle_persistence_only_bot(): return PicklePersistence( - filename='pickletest', + filepath='pickletest', store_data=PersistenceInput(callback_data=False, user_data=False, chat_data=False), single_file=False, on_flush=False, @@ -890,7 +891,7 @@ def pickle_persistence_only_bot(): @pytest.fixture(scope='function') def pickle_persistence_only_chat(): return PicklePersistence( - filename='pickletest', + filepath='pickletest', store_data=PersistenceInput(callback_data=False, user_data=False, bot_data=False), single_file=False, on_flush=False, @@ -900,7 +901,7 @@ def pickle_persistence_only_chat(): @pytest.fixture(scope='function') def pickle_persistence_only_user(): return PicklePersistence( - filename='pickletest', + filepath='pickletest', store_data=PersistenceInput(callback_data=False, chat_data=False, bot_data=False), single_file=False, on_flush=False, @@ -910,7 +911,7 @@ def pickle_persistence_only_user(): @pytest.fixture(scope='function') def pickle_persistence_only_callback(): return PicklePersistence( - filename='pickletest', + filepath='pickletest', store_data=PersistenceInput(user_data=False, chat_data=False, bot_data=False), single_file=False, on_flush=False, @@ -927,8 +928,7 @@ def bad_pickle_files(): 'pickletest_conversations', 'pickletest', ]: - with open(name, 'w') as f: - f.write('(())') + Path(name).write_text('(())') yield True @@ -958,17 +958,17 @@ def good_pickle_files(user_data, chat_data, bot_data, callback_data, conversatio 'callback_data': callback_data, 'conversations': conversations, } - with open('pickletest_user_data', 'wb') as f: + with Path('pickletest_user_data').open('wb') as f: pickle.dump(user_data, f) - with open('pickletest_chat_data', 'wb') as f: + with Path('pickletest_chat_data').open('wb') as f: pickle.dump(chat_data, f) - with open('pickletest_bot_data', 'wb') as f: + with Path('pickletest_bot_data').open('wb') as f: pickle.dump(bot_data, f) - with open('pickletest_callback_data', 'wb') as f: + with Path('pickletest_callback_data').open('wb') as f: pickle.dump(callback_data, f) - with open('pickletest_conversations', 'wb') as f: + with Path('pickletest_conversations').open('wb') as f: pickle.dump(conversations, f) - with open('pickletest', 'wb') as f: + with Path('pickletest').open('wb') as f: pickle.dump(data, f) yield True @@ -981,15 +981,15 @@ def pickle_files_wo_bot_data(user_data, chat_data, callback_data, conversations) 'conversations': conversations, 'callback_data': callback_data, } - with open('pickletest_user_data', 'wb') as f: + with Path('pickletest_user_data').open('wb') as f: pickle.dump(user_data, f) - with open('pickletest_chat_data', 'wb') as f: + with Path('pickletest_chat_data').open('wb') as f: pickle.dump(chat_data, f) - with open('pickletest_callback_data', 'wb') as f: + with Path('pickletest_callback_data').open('wb') as f: pickle.dump(callback_data, f) - with open('pickletest_conversations', 'wb') as f: + with Path('pickletest_conversations').open('wb') as f: pickle.dump(conversations, f) - with open('pickletest', 'wb') as f: + with Path('pickletest').open('wb') as f: pickle.dump(data, f) yield True @@ -1002,15 +1002,15 @@ def pickle_files_wo_callback_data(user_data, chat_data, bot_data, conversations) 'bot_data': bot_data, 'conversations': conversations, } - with open('pickletest_user_data', 'wb') as f: + with Path('pickletest_user_data').open('wb') as f: pickle.dump(user_data, f) - with open('pickletest_chat_data', 'wb') as f: + with Path('pickletest_chat_data').open('wb') as f: pickle.dump(chat_data, f) - with open('pickletest_bot_data', 'wb') as f: + with Path('pickletest_bot_data').open('wb') as f: pickle.dump(bot_data, f) - with open('pickletest_conversations', 'wb') as f: + with Path('pickletest_conversations').open('wb') as f: pickle.dump(conversations, f) - with open('pickletest', 'wb') as f: + with Path('pickletest').open('wb') as f: pickle.dump(data, f) yield True @@ -1339,7 +1339,7 @@ class TestPicklePersistence: assert not pickle_persistence.user_data == user_data pickle_persistence.update_user_data(12345, user_data[12345]) assert pickle_persistence.user_data == user_data - with open('pickletest_user_data', 'rb') as f: + with Path('pickletest_user_data').open('rb') as f: user_data_test = defaultdict(dict, pickle.load(f)) assert user_data_test == user_data @@ -1351,7 +1351,7 @@ class TestPicklePersistence: assert not pickle_persistence.chat_data == chat_data pickle_persistence.update_chat_data(-12345, chat_data[-12345]) assert pickle_persistence.chat_data == chat_data - with open('pickletest_chat_data', 'rb') as f: + with Path('pickletest_chat_data').open('rb') as f: chat_data_test = defaultdict(dict, pickle.load(f)) assert chat_data_test == chat_data @@ -1363,7 +1363,7 @@ class TestPicklePersistence: assert not pickle_persistence.bot_data == bot_data pickle_persistence.update_bot_data(bot_data) assert pickle_persistence.bot_data == bot_data - with open('pickletest_bot_data', 'rb') as f: + with Path('pickletest_bot_data').open('rb') as f: bot_data_test = pickle.load(f) assert bot_data_test == bot_data @@ -1375,7 +1375,7 @@ class TestPicklePersistence: assert not pickle_persistence.callback_data == callback_data pickle_persistence.update_callback_data(callback_data) assert pickle_persistence.callback_data == callback_data - with open('pickletest_callback_data', 'rb') as f: + with Path('pickletest_callback_data').open('rb') as f: callback_data_test = pickle.load(f) assert callback_data_test == callback_data @@ -1385,7 +1385,7 @@ class TestPicklePersistence: pickle_persistence.update_conversation('name1', (123, 123), 5) assert pickle_persistence.conversations['name1'] == conversation1 assert pickle_persistence.get_conversations('name1') == conversation1 - with open('pickletest_conversations', 'rb') as f: + with Path('pickletest_conversations').open('rb') as f: conversations_test = defaultdict(dict, pickle.load(f)) assert conversations_test['name1'] == conversation1 @@ -1405,7 +1405,7 @@ class TestPicklePersistence: assert not pickle_persistence.user_data == user_data pickle_persistence.update_user_data(12345, user_data[12345]) assert pickle_persistence.user_data == user_data - with open('pickletest', 'rb') as f: + with Path('pickletest').open('rb') as f: user_data_test = defaultdict(dict, pickle.load(f)['user_data']) assert user_data_test == user_data @@ -1417,7 +1417,7 @@ class TestPicklePersistence: assert not pickle_persistence.chat_data == chat_data pickle_persistence.update_chat_data(-12345, chat_data[-12345]) assert pickle_persistence.chat_data == chat_data - with open('pickletest', 'rb') as f: + with Path('pickletest').open('rb') as f: chat_data_test = defaultdict(dict, pickle.load(f)['chat_data']) assert chat_data_test == chat_data @@ -1429,7 +1429,7 @@ class TestPicklePersistence: assert not pickle_persistence.bot_data == bot_data pickle_persistence.update_bot_data(bot_data) assert pickle_persistence.bot_data == bot_data - with open('pickletest', 'rb') as f: + with Path('pickletest').open('rb') as f: bot_data_test = pickle.load(f)['bot_data'] assert bot_data_test == bot_data @@ -1441,7 +1441,7 @@ class TestPicklePersistence: assert not pickle_persistence.callback_data == callback_data pickle_persistence.update_callback_data(callback_data) assert pickle_persistence.callback_data == callback_data - with open('pickletest', 'rb') as f: + with Path('pickletest').open('rb') as f: callback_data_test = pickle.load(f)['callback_data'] assert callback_data_test == callback_data @@ -1451,7 +1451,7 @@ class TestPicklePersistence: pickle_persistence.update_conversation('name1', (123, 123), 5) assert pickle_persistence.conversations['name1'] == conversation1 assert pickle_persistence.get_conversations('name1') == conversation1 - with open('pickletest', 'rb') as f: + with Path('pickletest').open('rb') as f: conversations_test = defaultdict(dict, pickle.load(f)['conversations']) assert conversations_test['name1'] == conversation1 @@ -1487,7 +1487,7 @@ class TestPicklePersistence: pickle_persistence.update_user_data(54321, user_data[54321]) assert pickle_persistence.user_data == user_data - with open('pickletest_user_data', 'rb') as f: + with Path('pickletest_user_data').open('rb') as f: user_data_test = defaultdict(dict, pickle.load(f)) assert not user_data_test == user_data @@ -1498,7 +1498,7 @@ class TestPicklePersistence: pickle_persistence.update_chat_data(54321, chat_data[54321]) assert pickle_persistence.chat_data == chat_data - with open('pickletest_chat_data', 'rb') as f: + with Path('pickletest_chat_data').open('rb') as f: chat_data_test = defaultdict(dict, pickle.load(f)) assert not chat_data_test == chat_data @@ -1509,7 +1509,7 @@ class TestPicklePersistence: pickle_persistence.update_bot_data(bot_data) assert pickle_persistence.bot_data == bot_data - with open('pickletest_bot_data', 'rb') as f: + with Path('pickletest_bot_data').open('rb') as f: bot_data_test = pickle.load(f) assert not bot_data_test == bot_data @@ -1520,7 +1520,7 @@ class TestPicklePersistence: pickle_persistence.update_callback_data(callback_data) assert pickle_persistence.callback_data == callback_data - with open('pickletest_callback_data', 'rb') as f: + with Path('pickletest_callback_data').open('rb') as f: callback_data_test = pickle.load(f) assert not callback_data_test == callback_data @@ -1531,24 +1531,24 @@ class TestPicklePersistence: pickle_persistence.update_conversation('name1', (123, 123), 5) assert pickle_persistence.conversations['name1'] == conversation1 - with open('pickletest_conversations', 'rb') as f: + with Path('pickletest_conversations').open('rb') as f: conversations_test = defaultdict(dict, pickle.load(f)) assert not conversations_test['name1'] == conversation1 pickle_persistence.flush() - with open('pickletest_user_data', 'rb') as f: + with Path('pickletest_user_data').open('rb') as f: user_data_test = defaultdict(dict, pickle.load(f)) assert user_data_test == user_data - with open('pickletest_chat_data', 'rb') as f: + with Path('pickletest_chat_data').open('rb') as f: chat_data_test = defaultdict(dict, pickle.load(f)) assert chat_data_test == chat_data - with open('pickletest_bot_data', 'rb') as f: + with Path('pickletest_bot_data').open('rb') as f: bot_data_test = pickle.load(f) assert bot_data_test == bot_data - with open('pickletest_conversations', 'rb') as f: + with Path('pickletest_conversations').open('rb') as f: conversations_test = defaultdict(dict, pickle.load(f)) assert conversations_test['name1'] == conversation1 @@ -1564,7 +1564,7 @@ class TestPicklePersistence: assert not pickle_persistence.user_data == user_data pickle_persistence.update_user_data(54321, user_data[54321]) assert pickle_persistence.user_data == user_data - with open('pickletest', 'rb') as f: + with Path('pickletest').open('rb') as f: user_data_test = defaultdict(dict, pickle.load(f)['user_data']) assert not user_data_test == user_data @@ -1573,7 +1573,7 @@ class TestPicklePersistence: assert not pickle_persistence.chat_data == chat_data pickle_persistence.update_chat_data(54321, chat_data[54321]) assert pickle_persistence.chat_data == chat_data - with open('pickletest', 'rb') as f: + with Path('pickletest').open('rb') as f: chat_data_test = defaultdict(dict, pickle.load(f)['chat_data']) assert not chat_data_test == chat_data @@ -1582,7 +1582,7 @@ class TestPicklePersistence: assert not pickle_persistence.bot_data == bot_data pickle_persistence.update_bot_data(bot_data) assert pickle_persistence.bot_data == bot_data - with open('pickletest', 'rb') as f: + with Path('pickletest').open('rb') as f: bot_data_test = pickle.load(f)['bot_data'] assert not bot_data_test == bot_data @@ -1591,7 +1591,7 @@ class TestPicklePersistence: assert not pickle_persistence.callback_data == callback_data pickle_persistence.update_callback_data(callback_data) assert pickle_persistence.callback_data == callback_data - with open('pickletest', 'rb') as f: + with Path('pickletest').open('rb') as f: callback_data_test = pickle.load(f)['callback_data'] assert not callback_data_test == callback_data @@ -1600,24 +1600,24 @@ class TestPicklePersistence: assert not pickle_persistence.conversations['name1'] == conversation1 pickle_persistence.update_conversation('name1', (123, 123), 5) assert pickle_persistence.conversations['name1'] == conversation1 - with open('pickletest', 'rb') as f: + with Path('pickletest').open('rb') as f: conversations_test = defaultdict(dict, pickle.load(f)['conversations']) assert not conversations_test['name1'] == conversation1 pickle_persistence.flush() - with open('pickletest', 'rb') as f: + with Path('pickletest').open('rb') as f: user_data_test = defaultdict(dict, pickle.load(f)['user_data']) assert user_data_test == user_data - with open('pickletest', 'rb') as f: + with Path('pickletest').open('rb') as f: chat_data_test = defaultdict(dict, pickle.load(f)['chat_data']) assert chat_data_test == chat_data - with open('pickletest', 'rb') as f: + with Path('pickletest').open('rb') as f: bot_data_test = pickle.load(f)['bot_data'] assert bot_data_test == bot_data - with open('pickletest', 'rb') as f: + with Path('pickletest').open('rb') as f: conversations_test = defaultdict(dict, pickle.load(f)['conversations']) assert conversations_test['name1'] == conversation1 @@ -1656,7 +1656,7 @@ class TestPicklePersistence: dp.add_handler(h1) dp.process_update(update) pickle_persistence_2 = PicklePersistence( - filename='pickletest', + filepath='pickletest', single_file=False, on_flush=False, ) @@ -1675,7 +1675,7 @@ class TestPicklePersistence: dp.bot.callback_data_cache._callback_queries['test'] = 'Working4!' u._signal_handler(signal.SIGINT, None) pickle_persistence_2 = PicklePersistence( - filename='pickletest', + filepath='pickletest', single_file=False, on_flush=False, ) @@ -1695,7 +1695,7 @@ class TestPicklePersistence: dp.bot.callback_data_cache._callback_queries['test'] = 'Working4!' u._signal_handler(signal.SIGINT, None) pickle_persistence_2 = PicklePersistence( - filename='pickletest', + filepath='pickletest', store_data=PersistenceInput(callback_data=False, chat_data=False, user_data=False), single_file=False, on_flush=False, @@ -1715,7 +1715,7 @@ class TestPicklePersistence: dp.bot.callback_data_cache._callback_queries['test'] = 'Working4!' u._signal_handler(signal.SIGINT, None) pickle_persistence_2 = PicklePersistence( - filename='pickletest', + filepath='pickletest', store_data=PersistenceInput(callback_data=False, user_data=False, bot_data=False), single_file=False, on_flush=False, @@ -1735,7 +1735,7 @@ class TestPicklePersistence: dp.bot.callback_data_cache._callback_queries['test'] = 'Working4!' u._signal_handler(signal.SIGINT, None) pickle_persistence_2 = PicklePersistence( - filename='pickletest', + filepath='pickletest', store_data=PersistenceInput(callback_data=False, chat_data=False, bot_data=False), single_file=False, on_flush=False, @@ -1758,7 +1758,7 @@ class TestPicklePersistence: del u del pickle_persistence_only_callback pickle_persistence_2 = PicklePersistence( - filename='pickletest', + filepath='pickletest', store_data=PersistenceInput(user_data=False, chat_data=False, bot_data=False), single_file=False, on_flush=False, @@ -1852,6 +1852,21 @@ class TestPicklePersistence: assert nested_ch.conversations[nested_ch._get_key(update)] == 1 assert nested_ch.conversations == pickle_persistence.conversations['name3'] + @pytest.mark.parametrize( + 'filepath', + ['pickletest', Path('pickletest')], + ids=['str filepath', 'pathlib.Path filepath'], + ) + def test_filepath_argument_types(self, filepath): + pick_persist = PicklePersistence( + filepath=filepath, + on_flush=False, + ) + pick_persist.update_user_data(1, 1) + + assert pick_persist.get_user_data()[1] == 1 + assert Path(filepath).is_file() + def test_with_job(self, job_queue, dp, pickle_persistence): dp.bot.arbitrary_callback_data = True diff --git a/tests/test_photo.py b/tests/test_photo.py index baf34ddf3..d1b7f3046 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -43,7 +43,7 @@ def photo_file(): @pytest.fixture(scope='class') def _photo(bot, chat_id): def func(): - with open('tests/data/telegram.jpg', 'rb') as f: + with Path('tests/data/telegram.jpg').open('rb') as f: return bot.send_photo(chat_id, photo=f, timeout=50).photo return expect_bad_request(func, 'Type of file mismatch', 'Telegram did not accept the file.') @@ -288,7 +288,7 @@ class TestPhoto: new_file.download('telegram.jpg') - assert os.path.isfile('telegram.jpg') is True + assert Path('telegram.jpg').is_file() @flaky(3, 1) def test_send_url_jpg_file(self, bot, chat_id, thumb, photo): @@ -343,7 +343,7 @@ class TestPhoto: """ Regression test for https://github.com/python-telegram-bot/python-telegram-bot/issues/1202 """ - with open('tests/data/测试.png', 'rb') as f: + with Path('tests/data/测试.png').open('rb') as f: message = bot.send_photo(photo=f, chat_id=chat_id) photo = message.photo[-1] @@ -356,21 +356,21 @@ class TestPhoto: @flaky(3, 1) def test_send_bytesio_jpg_file(self, bot, chat_id): - file_name = 'tests/data/telegram_no_standard_header.jpg' + filepath: Path = Path('tests/data/telegram_no_standard_header.jpg') # raw image bytes - raw_bytes = BytesIO(open(file_name, 'rb').read()) + raw_bytes = BytesIO(filepath.read_bytes()) input_file = InputFile(raw_bytes) assert input_file.mimetype == 'application/octet-stream' # raw image bytes with name info - raw_bytes = BytesIO(open(file_name, 'rb').read()) - raw_bytes.name = file_name + raw_bytes = BytesIO(filepath.read_bytes()) + raw_bytes.name = str(filepath) input_file = InputFile(raw_bytes) assert input_file.mimetype == 'image/jpeg' # send raw photo - raw_bytes = BytesIO(open(file_name, 'rb').read()) + raw_bytes = BytesIO(filepath.read_bytes()) message = bot.send_photo(chat_id, photo=raw_bytes) photo = message.photo[-1] assert isinstance(photo.file_id, str) diff --git a/tests/test_request.py b/tests/test_request.py index 03ebd8e1e..70d4ec5b1 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -16,6 +16,8 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +from pathlib import Path + import pytest from telegram.error import TelegramError @@ -48,3 +50,16 @@ def test_parse_illegal_json(): with pytest.raises(TelegramError, match='Invalid server response'): Request._parse(server_response) + + +@pytest.mark.parametrize( + "destination_path_type", + [str, Path], + ids=['str destination_path', 'pathlib.Path destination_path'], +) +def test_download(destination_path_type): + destination_filepath = Path.cwd() / 'tests' / 'data' / 'downloaded_request.txt' + request = Request() + request.download("http://google.com", destination_path_type(destination_filepath)) + assert destination_filepath.is_file() + destination_filepath.unlink() diff --git a/tests/test_sticker.py b/tests/test_sticker.py index 7088b409a..905278c83 100644 --- a/tests/test_sticker.py +++ b/tests/test_sticker.py @@ -30,39 +30,37 @@ from tests.conftest import check_shortcut_call, check_shortcut_signature, check_ @pytest.fixture(scope='function') def sticker_file(): - f = open('tests/data/telegram.webp', 'rb') - yield f - f.close() + with Path('tests/data/telegram.webp').open('rb') as file: + yield file @pytest.fixture(scope='class') def sticker(bot, chat_id): - with open('tests/data/telegram.webp', 'rb') as f: + with Path('tests/data/telegram.webp').open('rb') as f: return bot.send_sticker(chat_id, sticker=f, timeout=50).sticker @pytest.fixture(scope='function') def animated_sticker_file(): - f = open('tests/data/telegram_animated_sticker.tgs', 'rb') - yield f - f.close() + with Path('tests/data/telegram_animated_sticker.tgs').open('rb') as f: + yield f @pytest.fixture(scope='class') def animated_sticker(bot, chat_id): - with open('tests/data/telegram_animated_sticker.tgs', 'rb') as f: + with Path('tests/data/telegram_animated_sticker.tgs').open('rb') as f: return bot.send_sticker(chat_id, sticker=f, timeout=50).sticker @pytest.fixture(scope='function') def video_sticker_file(): - with open('tests/data/telegram_video_sticker.webm', 'rb') as f: + with Path('tests/data/telegram_video_sticker.webm').open('rb') as f: yield f @pytest.fixture(scope='class') def video_sticker(bot, chat_id): - with open('tests/data/telegram_video_sticker.webm', 'rb') as f: + with Path('tests/data/telegram_video_sticker.webm').open('rb') as f: return bot.send_sticker(chat_id, sticker=f, timeout=50).sticker @@ -153,7 +151,7 @@ class TestSticker: new_file.download('telegram.webp') - assert os.path.isfile('telegram.webp') + assert Path('telegram.webp').is_file() @flaky(3, 1) def test_resend(self, bot, chat_id, sticker): @@ -377,9 +375,8 @@ def video_sticker_set(bot): @pytest.fixture(scope='function') def sticker_set_thumb_file(): - f = open('tests/data/sticker_set_thumb.png', 'rb') - yield f - f.close() + with Path('tests/data/sticker_set_thumb.png').open('rb') as file: + yield file class TestStickerSet: @@ -455,7 +452,7 @@ class TestStickerSet: @flaky(3, 1) def test_bot_methods_1_png(self, bot, chat_id, sticker_file): - with open('tests/data/telegram_sticker.png', 'rb') as f: + with Path('tests/data/telegram_sticker.png').open('rb') as f: # chat_id was hardcoded as 95205500 but it stopped working for some reason file = bot.upload_sticker_file(chat_id, f) assert file @@ -476,13 +473,13 @@ class TestStickerSet: assert bot.add_sticker_to_set( chat_id, f'animated_test_by_{bot.username}', - tgs_sticker=open('tests/data/telegram_animated_sticker.tgs', 'rb'), + tgs_sticker=Path('tests/data/telegram_animated_sticker.tgs').open('rb'), emojis='😄', ) @flaky(3, 1) def test_bot_methods_1_webm(self, bot, chat_id): - with open('tests/data/telegram_video_sticker.webm', 'rb') as f: + with Path('tests/data/telegram_video_sticker.webm').open('rb') as f: assert bot.add_sticker_to_set( chat_id, f'video_test_by_{bot.username}', webm_sticker=f, emojis='🤔' ) diff --git a/tests/test_video.py b/tests/test_video.py index ff02e0394..78820cf58 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -30,14 +30,14 @@ from tests.conftest import check_shortcut_call, check_shortcut_signature, check_ @pytest.fixture(scope='function') def video_file(): - f = open('tests/data/telegram.mp4', 'rb') + f = Path('tests/data/telegram.mp4').open('rb') yield f f.close() @pytest.fixture(scope='class') def video(bot, chat_id): - with open('tests/data/telegram.mp4', 'rb') as f: + with Path('tests/data/telegram.mp4').open('rb') as f: return bot.send_video(chat_id, video=f, timeout=50).video @@ -141,7 +141,7 @@ class TestVideo: new_file.download('telegram.mp4') - assert os.path.isfile('telegram.mp4') + assert Path('telegram.mp4').is_file() @flaky(3, 1) def test_send_mp4_file_url(self, bot, chat_id, video): diff --git a/tests/test_videonote.py b/tests/test_videonote.py index ca60968be..ac38ce357 100644 --- a/tests/test_videonote.py +++ b/tests/test_videonote.py @@ -36,7 +36,7 @@ def video_note_file(): @pytest.fixture(scope='class') def video_note(bot, chat_id): - with open('tests/data/telegram2.mp4', 'rb') as f: + with Path('tests/data/telegram2.mp4').open('rb') as f: return bot.send_video_note(chat_id, video_note=f, timeout=50).video_note @@ -123,7 +123,7 @@ class TestVideoNote: new_file.download('telegram2.mp4') - assert os.path.isfile('telegram2.mp4') + assert Path('telegram2.mp4').is_file() @flaky(3, 1) def test_resend(self, bot, chat_id, video_note): diff --git a/tests/test_voice.py b/tests/test_voice.py index ce263584a..a905570ea 100644 --- a/tests/test_voice.py +++ b/tests/test_voice.py @@ -30,14 +30,14 @@ from tests.conftest import check_shortcut_call, check_shortcut_signature, check_ @pytest.fixture(scope='function') def voice_file(): - f = open('tests/data/telegram.ogg', 'rb') + f = Path('tests/data/telegram.ogg').open('rb') yield f f.close() @pytest.fixture(scope='class') def voice(bot, chat_id): - with open('tests/data/telegram.ogg', 'rb') as f: + with Path('tests/data/telegram.ogg').open('rb') as f: return bot.send_voice(chat_id, voice=f, timeout=50).voice @@ -111,9 +111,9 @@ class TestVoice: assert new_file.file_unique_id == voice.file_unique_id assert new_file.file_path.startswith('https://') - new_file.download('telegram.ogg') + new_filepath = new_file.download('telegram.ogg') - assert os.path.isfile('telegram.ogg') + assert new_filepath.is_file() @flaky(3, 1) def test_send_ogg_url_file(self, bot, chat_id, voice):