diff --git a/telegram/ext/_applicationbuilder.py b/telegram/ext/_applicationbuilder.py index 4d7d8a223..5a5ac0402 100644 --- a/telegram/ext/_applicationbuilder.py +++ b/telegram/ext/_applicationbuilder.py @@ -32,9 +32,12 @@ from typing import ( Union, ) +import httpx + from telegram._bot import Bot from telegram._utils.defaultvalue import DEFAULT_FALSE, DEFAULT_NONE, DefaultValue from telegram._utils.types import DVInput, DVType, FilePathInput, HTTPVersion, ODVInput +from telegram._utils.warnings import warn from telegram.ext._application import Application from telegram.ext._baseupdateprocessor import BaseUpdateProcessor, SimpleUpdateProcessor from telegram.ext._contexttypes import ContextTypes @@ -44,6 +47,7 @@ from telegram.ext._updater import Updater from telegram.ext._utils.types import BD, BT, CCT, CD, JQ, UD from telegram.request import BaseRequest from telegram.request._httpxrequest import HTTPXRequest +from telegram.warnings import PTBDeprecationWarning if TYPE_CHECKING: from telegram.ext import BasePersistence, BaseRateLimiter, CallbackContext, Defaults @@ -66,14 +70,14 @@ _BOT_CHECKS = [ ("request", "request instance"), ("get_updates_request", "get_updates_request instance"), ("connection_pool_size", "connection_pool_size"), - ("proxy_url", "proxy_url"), + ("proxy", "proxy"), ("pool_timeout", "pool_timeout"), ("connect_timeout", "connect_timeout"), ("read_timeout", "read_timeout"), ("write_timeout", "write_timeout"), ("http_version", "http_version"), ("get_updates_connection_pool_size", "get_updates_connection_pool_size"), - ("get_updates_proxy_url", "get_updates_proxy_url"), + ("get_updates_proxy", "get_updates_proxy"), ("get_updates_pool_timeout", "get_updates_pool_timeout"), ("get_updates_connect_timeout", "get_updates_connect_timeout"), ("get_updates_read_timeout", "get_updates_read_timeout"), @@ -136,7 +140,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): "_get_updates_connect_timeout", "_get_updates_connection_pool_size", "_get_updates_pool_timeout", - "_get_updates_proxy_url", + "_get_updates_proxy", "_get_updates_read_timeout", "_get_updates_request", "_get_updates_write_timeout", @@ -149,7 +153,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): "_post_stop", "_private_key", "_private_key_password", - "_proxy_url", + "_proxy", "_rate_limiter", "_read_timeout", "_request", @@ -166,14 +170,14 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): self._base_url: DVType[str] = DefaultValue("https://api.telegram.org/bot") self._base_file_url: DVType[str] = DefaultValue("https://api.telegram.org/file/bot") self._connection_pool_size: DVInput[int] = DEFAULT_NONE - self._proxy_url: DVInput[str] = DEFAULT_NONE + self._proxy: DVInput[Union[str, httpx.Proxy, httpx.URL]] = DEFAULT_NONE self._connect_timeout: ODVInput[float] = DEFAULT_NONE self._read_timeout: ODVInput[float] = DEFAULT_NONE self._write_timeout: ODVInput[float] = DEFAULT_NONE self._pool_timeout: ODVInput[float] = DEFAULT_NONE self._request: DVInput[BaseRequest] = DEFAULT_NONE self._get_updates_connection_pool_size: DVInput[int] = DEFAULT_NONE - self._get_updates_proxy_url: DVInput[str] = DEFAULT_NONE + self._get_updates_proxy: DVInput[Union[str, httpx.Proxy, httpx.URL]] = DEFAULT_NONE self._get_updates_connect_timeout: ODVInput[float] = DEFAULT_NONE self._get_updates_read_timeout: ODVInput[float] = DEFAULT_NONE self._get_updates_write_timeout: ODVInput[float] = DEFAULT_NONE @@ -214,7 +218,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): if not isinstance(getattr(self, f"{prefix}request"), DefaultValue): return getattr(self, f"{prefix}request") - proxy_url = DefaultValue.get_value(getattr(self, f"{prefix}proxy_url")) + proxy = DefaultValue.get_value(getattr(self, f"{prefix}proxy")) if get_updates: connection_pool_size = ( DefaultValue.get_value(getattr(self, f"{prefix}connection_pool_size")) or 1 @@ -239,7 +243,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): return HTTPXRequest( connection_pool_size=connection_pool_size, - proxy_url=proxy_url, + proxy=proxy, http_version=http_version, # type: ignore[arg-type] **effective_timeouts, ) @@ -419,8 +423,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): if not isinstance(getattr(self, f"_{prefix}connection_pool_size"), DefaultValue): raise RuntimeError(_TWO_ARGS_REQ.format(name, "connection_pool_size")) - if not isinstance(getattr(self, f"_{prefix}proxy_url"), DefaultValue): - raise RuntimeError(_TWO_ARGS_REQ.format(name, "proxy_url")) + if not isinstance(getattr(self, f"_{prefix}proxy"), DefaultValue): + raise RuntimeError(_TWO_ARGS_REQ.format(name, "proxy")) if not isinstance(getattr(self, f"_{prefix}http_version"), DefaultValue): raise RuntimeError(_TWO_ARGS_REQ.format(name, "http_version")) @@ -486,21 +490,45 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): return self def proxy_url(self: BuilderType, proxy_url: str) -> BuilderType: - """Sets the proxy for the :paramref:`~telegram.request.HTTPXRequest.proxy_url` - parameter of :attr:`telegram.Bot.request`. Defaults to :obj:`None`. + """Legacy name for :meth:`proxy`, kept for backward compatibility. + .. seealso:: :meth:`get_updates_proxy` - .. seealso:: :meth:`get_updates_proxy_url` + .. deprecated:: NEXT.VERSION Args: - proxy_url (:obj:`str`): The URL to the proxy server. See - :paramref:`telegram.request.HTTPXRequest.proxy_url` for more information. + proxy_url (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): See + :paramref:`telegram.ext.ApplicationBuilder.proxy.proxy`. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ - self._request_param_check(name="proxy_url", get_updates=False) - self._proxy_url = proxy_url + warn( + "`ApplicationBuilder.proxy_url` is deprecated since version " + "NEXT.VERSION. Use `ApplicationBuilder.proxy` instead.", + PTBDeprecationWarning, + stacklevel=2, + ) + return self.proxy(proxy_url) + + def proxy(self: BuilderType, proxy: Union[str, httpx.Proxy, httpx.URL]) -> BuilderType: + """Sets the proxy for the :paramref:`~telegram.request.HTTPXRequest.proxy` + parameter of :attr:`telegram.Bot.request`. Defaults to :obj:`None`. + + .. seealso:: :meth:`get_updates_proxy` + + .. versionadded:: NEXT.VERSION + + Args: + proxy (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): The URL to a proxy + server, a ``httpx.Proxy`` object or a ``httpx.URL`` object. See + :paramref:`telegram.request.HTTPXRequest.proxy` for more information. + + Returns: + :class:`ApplicationBuilder`: The same builder with the updated argument. + """ + self._request_param_check(name="proxy", get_updates=False) + self._proxy = proxy return self def connect_timeout(self: BuilderType, connect_timeout: Optional[float]) -> BuilderType: @@ -655,21 +683,47 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): return self def get_updates_proxy_url(self: BuilderType, get_updates_proxy_url: str) -> BuilderType: - """Sets the proxy for the :paramref:`telegram.request.HTTPXRequest.proxy_url` - parameter which is used for :meth:`telegram.Bot.get_updates`. Defaults to :obj:`None`. + """Legacy name for :meth:`get_updates_proxy`, kept for backward compatibility. + .. seealso:: :meth:`proxy` - .. seealso:: :meth:`proxy_url` + .. deprecated:: NEXT.VERSION Args: - get_updates_proxy_url (:obj:`str`): The URL to the proxy server. See - :paramref:`telegram.request.HTTPXRequest.proxy_url` for more information. + get_updates_proxy_url (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): See + :paramref:`telegram.ext.ApplicationBuilder.get_updates_proxy.get_updates_proxy`. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ - self._request_param_check(name="proxy_url", get_updates=True) - self._get_updates_proxy_url = get_updates_proxy_url + warn( + "`ApplicationBuilder.get_updates_proxy_url` is deprecated since version " + "NEXT.VERSION. Use `ApplicationBuilder.get_updates_proxy` instead.", + PTBDeprecationWarning, + stacklevel=2, + ) + return self.get_updates_proxy(get_updates_proxy_url) + + def get_updates_proxy( + self: BuilderType, get_updates_proxy: Union[str, httpx.Proxy, httpx.URL] + ) -> BuilderType: + """Sets the proxy for the :paramref:`telegram.request.HTTPXRequest.proxy` + parameter which is used for :meth:`telegram.Bot.get_updates`. Defaults to :obj:`None`. + + .. seealso:: :meth:`proxy` + + .. versionadded:: NEXT.VERSION + + Args: + proxy (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): The URL to a proxy server, + a ``httpx.Proxy`` object or a ``httpx.URL`` object. See + :paramref:`telegram.request.HTTPXRequest.proxy` for more information. + + Returns: + :class:`ApplicationBuilder`: The same builder with the updated argument. + """ + self._request_param_check(name="proxy", get_updates=True) + self._get_updates_proxy = get_updates_proxy return self def get_updates_connect_timeout( diff --git a/telegram/request/_httpxrequest.py b/telegram/request/_httpxrequest.py index ae2673e5d..1560b8981 100644 --- a/telegram/request/_httpxrequest.py +++ b/telegram/request/_httpxrequest.py @@ -24,9 +24,11 @@ import httpx from telegram._utils.defaultvalue import DefaultValue from telegram._utils.logging import get_logger from telegram._utils.types import HTTPVersion, ODVInput +from telegram._utils.warnings import warn from telegram.error import NetworkError, TimedOut from telegram.request._baserequest import BaseRequest from telegram.request._requestdata import RequestData +from telegram.warnings import PTBDeprecationWarning # Note to future devs: # Proxies are currently only tested manually. The httpx development docs have a nice guide on that: @@ -55,17 +57,10 @@ class HTTPXRequest(BaseRequest): Note: Independent of the value, one additional connection will be reserved for :meth:`telegram.Bot.get_updates`. - proxy_url (:obj:`str`, optional): The URL to the proxy server. For example - ``'http://127.0.0.1:3128'`` or ``'socks5://127.0.0.1:3128'``. Defaults to :obj:`None`. + proxy_url (:obj:`str`, optional): Legacy name for :paramref:`proxy`, kept for backward + compatibility. Defaults to :obj:`None`. - Note: - * The proxy URL can also be set via the environment variables ``HTTPS_PROXY`` or - ``ALL_PROXY``. See `the docs of httpx`_ for more info. - * For Socks5 support, additional dependencies are required. Make sure to install - PTB via :command:`pip install "python-telegram-bot[socks]"` in this case. - * Socks5 proxies can not be set via environment variables. - - .. _the docs of httpx: https://www.python-httpx.org/environment_variables/#proxies + .. deprecated:: NEXT.VERSION read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a response from Telegram's server. This value is used unless a different value is passed to :meth:`do_request`. @@ -107,6 +102,22 @@ class HTTPXRequest(BaseRequest): these concepts. .. versionadded:: NEXT.VERSION + proxy (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``, optional): The URL to a proxy server, + a ``httpx.Proxy`` object or a ``httpx.URL`` object. For example + ``'http://127.0.0.1:3128'`` or ``'socks5://127.0.0.1:3128'``. Defaults to :obj:`None`. + + Note: + * The proxy URL can also be set via the environment variables ``HTTPS_PROXY`` or + ``ALL_PROXY``. See `the docs of httpx`_ for more info. + * HTTPS proxies can be configured by passing a ``httpx.Proxy`` object with + a corresponding ``ssl_context``. + * For Socks5 support, additional dependencies are required. Make sure to install + PTB via :command:`pip install "python-telegram-bot[socks]"` in this case. + * Socks5 proxies can not be set via environment variables. + + .. _the docs of httpx: https://www.python-httpx.org/environment_variables/#proxies + + .. versionadded:: NEXT.VERSION """ @@ -115,14 +126,27 @@ class HTTPXRequest(BaseRequest): def __init__( self, connection_pool_size: int = 1, - proxy_url: Optional[str] = None, + proxy_url: Optional[Union[str, httpx.Proxy, httpx.URL]] = None, read_timeout: Optional[float] = 5.0, write_timeout: Optional[float] = 5.0, connect_timeout: Optional[float] = 5.0, pool_timeout: Optional[float] = 1.0, http_version: HTTPVersion = "1.1", socket_options: Optional[Collection[_SocketOpt]] = None, + proxy: Optional[Union[str, httpx.Proxy, httpx.URL]] = None, ): + if proxy_url is not None and proxy is not None: + raise ValueError("The parameters `proxy_url` and `proxy` are mutually exclusive.") + + if proxy_url is not None: + proxy = proxy_url + warn( + "The parameter `proxy_url` is deprecated since version NEXT.VERSION. Use `proxy` " + "instead.", + PTBDeprecationWarning, + stacklevel=2, + ) + self._http_version = http_version timeout = httpx.Timeout( connect=connect_timeout, @@ -149,7 +173,7 @@ class HTTPXRequest(BaseRequest): ) self._client_kwargs = { "timeout": timeout, - "proxies": proxy_url, + "proxies": proxy, "limits": limits, "transport": transport, **http_kwargs, diff --git a/tests/ext/test_applicationbuilder.py b/tests/ext/test_applicationbuilder.py index 6f13b8178..0ac7ffca3 100644 --- a/tests/ext/test_applicationbuilder.py +++ b/tests/ext/test_applicationbuilder.py @@ -37,6 +37,7 @@ from telegram.ext import ( from telegram.ext._applicationbuilder import _BOT_CHECKS from telegram.ext._baseupdateprocessor import SimpleUpdateProcessor from telegram.request import HTTPXRequest +from telegram.warnings import PTBDeprecationWarning from tests.auxil.constants import PRIVATE_KEY from tests.auxil.envvars import TEST_WITH_OPT_DEPS from tests.auxil.files import data_file @@ -169,6 +170,7 @@ class TestApplicationBuilder: "pool_timeout", "read_timeout", "write_timeout", + "proxy", "proxy_url", "bot", "updater", @@ -178,8 +180,9 @@ class TestApplicationBuilder: def test_mutually_exclusive_for_request(self, builder, method): builder.request(1) + method_name = method.replace("proxy_url", "proxy") with pytest.raises( - RuntimeError, match=f"`{method}` may only be set, if no request instance" + RuntimeError, match=f"`{method_name}` may only be set, if no request instance" ): getattr(builder, method)(data_file("private.key")) @@ -196,6 +199,7 @@ class TestApplicationBuilder: "get_updates_pool_timeout", "get_updates_read_timeout", "get_updates_write_timeout", + "get_updates_proxy", "get_updates_proxy_url", "get_updates_http_version", "bot", @@ -205,9 +209,10 @@ class TestApplicationBuilder: def test_mutually_exclusive_for_get_updates_request(self, builder, method): builder.get_updates_request(1) + method_name = method.replace("proxy_url", "proxy") with pytest.raises( RuntimeError, - match=f"`{method}` may only be set, if no get_updates_request instance", + match=f"`{method_name}` may only be set, if no get_updates_request instance", ): getattr(builder, method)(data_file("private.key")) @@ -225,12 +230,14 @@ class TestApplicationBuilder: "get_updates_read_timeout", "get_updates_write_timeout", "get_updates_proxy_url", + "get_updates_proxy", "get_updates_http_version", "connection_pool_size", "connect_timeout", "pool_timeout", "read_timeout", "write_timeout", + "proxy", "proxy_url", "http_version", "bot", @@ -242,14 +249,17 @@ class TestApplicationBuilder: def test_mutually_exclusive_for_updater(self, builder, method): builder.updater(1) + method_name = method.replace("proxy_url", "proxy") with pytest.raises( RuntimeError, - match=f"`{method}` may only be set, if no updater", + match=f"`{method_name}` may only be set, if no updater", ): getattr(builder, method)(data_file("private.key")) builder = ApplicationBuilder() getattr(builder, method)(data_file("private.key")) + + method = method.replace("proxy_url", "proxy") with pytest.raises(RuntimeError, match=f"`updater` may only be set, if no {method}"): builder.updater(1) @@ -261,6 +271,7 @@ class TestApplicationBuilder: "get_updates_pool_timeout", "get_updates_read_timeout", "get_updates_write_timeout", + "get_updates_proxy", "get_updates_proxy_url", "get_updates_http_version", "connection_pool_size", @@ -268,6 +279,7 @@ class TestApplicationBuilder: "pool_timeout", "read_timeout", "write_timeout", + "proxy", "proxy_url", "bot", "http_version", @@ -285,7 +297,15 @@ class TestApplicationBuilder: getattr(builder, method)(data_file("private.key")) builder.updater(None) - def test_all_bot_args_custom(self, builder, bot, monkeypatch): + # We test with bot the new & legacy version to ensure that the legacy version still works + @pytest.mark.parametrize( + ("proxy_method", "get_updates_proxy_method"), + [("proxy", "get_updates_proxy"), ("proxy_url", "get_updates_proxy_url")], + ids=["new", "legacy"], + ) + def test_all_bot_args_custom( + self, builder, bot, monkeypatch, proxy_method, get_updates_proxy_method + ): defaults = Defaults() request = HTTPXRequest() get_updates_request = HTTPXRequest() @@ -330,13 +350,14 @@ class TestApplicationBuilder: builder = ApplicationBuilder().token(bot.token) builder.connection_pool_size(1).connect_timeout(2).pool_timeout(3).read_timeout( 4 - ).write_timeout(5).proxy_url("proxy_url").http_version("1.1") + ).write_timeout(5).http_version("1.1") + getattr(builder, proxy_method)("proxy") app = builder.build() client = app.bot.request._client assert client.timeout == httpx.Timeout(pool=3, connect=2, read=4, write=5) assert client.limits == httpx.Limits(max_connections=1, max_keepalive_connections=1) - assert client.proxies == "proxy_url" + assert client.proxies == "proxy" assert client.http1 is True assert client.http2 is False @@ -345,17 +366,16 @@ class TestApplicationBuilder: 2 ).get_updates_pool_timeout(3).get_updates_read_timeout(4).get_updates_write_timeout( 5 - ).get_updates_proxy_url( - "proxy_url" ).get_updates_http_version( "1.1" ) + getattr(builder, get_updates_proxy_method)("get_updates_proxy") app = builder.build() client = app.bot._request[0]._client assert client.timeout == httpx.Timeout(pool=3, connect=2, read=4, write=5) assert client.limits == httpx.Limits(max_connections=1, max_keepalive_connections=1) - assert client.proxies == "proxy_url" + assert client.proxies == "get_updates_proxy" assert client.http1 is True assert client.http2 is False @@ -478,3 +498,19 @@ class TestApplicationBuilder: assert app.job_queue is None assert isinstance(app.update_queue, asyncio.Queue) assert isinstance(app.updater, Updater) + + def test_proxy_url_deprecation_warning(self, bot, builder, recwarn): + builder.token(bot.token).proxy_url("proxy_url") + assert len(recwarn) == 1 + assert "`ApplicationBuilder.proxy_url` is deprecated" in str(recwarn[0].message) + assert recwarn[0].category is PTBDeprecationWarning + assert recwarn[0].filename == __file__, "wrong stacklevel" + + def test_get_updates_proxy_url_deprecation_warning(self, bot, builder, recwarn): + builder.token(bot.token).get_updates_proxy_url("get_updates_proxy_url") + assert len(recwarn) == 1 + assert "`ApplicationBuilder.get_updates_proxy_url` is deprecated" in str( + recwarn[0].message + ) + assert recwarn[0].category is PTBDeprecationWarning + assert recwarn[0].filename == __file__, "wrong stacklevel" diff --git a/tests/request/test_request.py b/tests/request/test_request.py index 44c1dddbf..0d6d2f6e5 100644 --- a/tests/request/test_request.py +++ b/tests/request/test_request.py @@ -43,6 +43,7 @@ from telegram.error import ( TimedOut, ) from telegram.request._httpxrequest import HTTPXRequest +from telegram.warnings import PTBDeprecationWarning from tests.auxil.envvars import TEST_WITH_OPT_DEPS from tests.auxil.slots import mro_slots @@ -79,7 +80,7 @@ async def httpx_request(): class TestNoSocksHTTP2WithoutRequest: async def test_init(self, bot): with pytest.raises(RuntimeError, match=r"python-telegram-bot\[socks\]"): - HTTPXRequest(proxy_url="socks5://foo") + HTTPXRequest(proxy="socks5://foo") with pytest.raises(RuntimeError, match=r"python-telegram-bot\[http2\]"): HTTPXRequest(http_version="2") @@ -118,7 +119,7 @@ class TestRequestWithoutRequest: # Make sure that other exceptions are forwarded with pytest.raises(ImportError, match=r"Other Error Message"): - HTTPXRequest(proxy_url="socks5://foo") + HTTPXRequest(proxy="socks5://foo") def test_slot_behaviour(self): inst = HTTPXRequest() @@ -359,7 +360,9 @@ class TestHTTPXRequestWithoutRequest: def _reset(self): self.test_flag = None - def test_init(self, monkeypatch): + # We parametrize this to make sure that the legacy `proxy_url` argument is still supported + @pytest.mark.parametrize("proxy_argument", ["proxy", "proxy_url"]) + def test_init(self, monkeypatch, proxy_argument): @dataclass class Client: timeout: object @@ -380,20 +383,32 @@ class TestHTTPXRequestWithoutRequest: assert request._client.http1 is True assert not request._client.http2 - request = HTTPXRequest( - connection_pool_size=42, - proxy_url="proxy_url", - connect_timeout=43, - read_timeout=44, - write_timeout=45, - pool_timeout=46, - ) - assert request._client.proxies == "proxy_url" + kwargs = { + "connection_pool_size": 42, + proxy_argument: "proxy", + "connect_timeout": 43, + "read_timeout": 44, + "write_timeout": 45, + "pool_timeout": 46, + } + request = HTTPXRequest(**kwargs) + assert request._client.proxies == "proxy" assert request._client.limits == httpx.Limits( max_connections=42, max_keepalive_connections=42 ) assert request._client.timeout == httpx.Timeout(connect=43, read=44, write=45, pool=46) + def test_proxy_mutually_exclusive(self): + with pytest.raises(ValueError, match="mutually exclusive"): + HTTPXRequest(proxy="proxy", proxy_url="proxy_url") + + def test_proxy_url_deprecation_warning(self, recwarn): + HTTPXRequest(proxy_url="http://127.0.0.1:3128") + assert len(recwarn) == 1 + assert recwarn[0].category is PTBDeprecationWarning + assert "`proxy_url` is deprecated" in str(recwarn[0].message) + assert recwarn[0].filename == __file__, "incorrect stacklevel" + async def test_multiple_inits_and_shutdowns(self, monkeypatch): self.test_flag = defaultdict(int)