diff --git a/news/5378.feature.rst b/news/5378.feature.rst new file mode 100644 index 00000000000..99982077909 --- /dev/null +++ b/news/5378.feature.rst @@ -0,0 +1 @@ +Add ``--no-proxy`` option to bypass http proxy. Using this option will ignore any configured http proxies, including any environmental variables diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 0b7cff77bdd..f8e07baf33e 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -274,6 +274,15 @@ class PipOption(Option): help="Specify a proxy in the form scheme://[user:passwd@]proxy.server:port.", ) +no_proxy: Callable[..., Option] = partial( + Option, + "--no-proxy", + dest="no_proxy", + action="store_true", + default=False, + help="Ignore all configured proxy settings, including environmental variables.", +) + retries: Callable[..., Option] = partial( Option, "--retries", @@ -1048,6 +1057,7 @@ def check_list_path_option(options: Values) -> None: no_input, keyring_provider, proxy, + no_proxy, retries, timeout, exists_action, diff --git a/src/pip/_internal/cli/index_command.py b/src/pip/_internal/cli/index_command.py index db105d0fef9..89dbfc42e8e 100644 --- a/src/pip/_internal/cli/index_command.py +++ b/src/pip/_internal/cli/index_command.py @@ -124,6 +124,19 @@ def _build_session( } session.trust_env = False + # Handle no proxy option + if options.no_proxy: + # Handle case of both --no-proxy being set along with --proxy=. + # In this case, the proxies from the environmental variables will be + # ignored, but the command line proxy will be used. + http_proxy = options.proxy if options.proxy else None + https_proxy = options.proxy if options.proxy else None + session.proxies = { + "http": http_proxy, + "https": https_proxy, + } + session.trust_env = False + # Determine if we can prompt the user for authentication or not session.auth.prompting = not options.no_input session.auth.keyring_provider = options.keyring_provider diff --git a/tests/conftest.py b/tests/conftest.py index d093eea462b..b4f5993ac47 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -96,6 +96,12 @@ def pytest_addoption(parser: Parser) -> None: default=None, help="use given proxy in session network tests", ) + parser.addoption( + "--no-proxy", + action="store_true", + default=False, + help="ignore any configured proxies in session network tests", + ) parser.addoption( "--use-zipapp", action="store_true", diff --git a/tests/unit/test_network_session.py b/tests/unit/test_network_session.py index fd00d5c606c..e54ff9f80a1 100644 --- a/tests/unit/test_network_session.py +++ b/tests/unit/test_network_session.py @@ -1,7 +1,7 @@ import logging import os from pathlib import Path -from typing import Any, List, Optional +from typing import Any, List, Optional, Union from urllib.parse import urlparse from urllib.request import getproxies @@ -282,3 +282,39 @@ def test_proxy(self, proxy: Optional[str]) -> None: f"Invalid proxy {proxy} or session.proxies: " f"{session.proxies} is not correctly passed to session.request." ) + + @pytest.mark.network + def test_no_proxy(self) -> None: + def _set_no_proxy(session: PipSession) -> PipSession: + """Mimic logic for command line `no_proxy` option""" + session.trust_env = False + session.proxies = { + "http": None, + "https": None, + } + return session + + session = PipSession(trusted_hosts=[]) + + connection_error_http: Union[requests.exceptions.RequestException, None] = None + # setup with known bad (hopefully) http proxy, and then test connection + # expecting a failure if the proxy is used + with requests.utils.set_environ("http_proxy", "http://127.0.0.1:88888"): + try: + session = _set_no_proxy(session) + session.request("GET", "https://pypi.org", timeout=1) + except requests.exceptions.ConnectionError as e: + connection_error_http = e + + connection_error_https: Union[requests.exceptions.RequestException, None] = None + # setup with known bad (hopefully) https proxy, and then test connection + # expecting a failure if the proxy is used + with requests.utils.set_environ("https_proxy", "http://127.0.0.1:65534"): + try: + session = _set_no_proxy(session) + session.request("GET", "https://pypi.org", timeout=1) + except requests.exceptions.ConnectionError as e: + connection_error_https = e + + assert connection_error_http is None, "Unexpected use of http proxy" + assert connection_error_https is None, "Unexpected use of https proxy" diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py index 8f3cf7de6a6..5de6092b210 100644 --- a/tests/unit/test_options.py +++ b/tests/unit/test_options.py @@ -523,6 +523,13 @@ def test_proxy(self) -> None: ) assert options1.proxy == options2.proxy == "path" + def test_no_proxy(self) -> None: + # FakeCommand intentionally returns the wrong type. + options1, _ = cast(Tuple[Values, List[str]], main(["--no-proxy", "fake"])) + options2, _ = cast(Tuple[Values, List[str]], main(["fake", "--no-proxy"])) + assert options1.no_proxy + assert options2.no_proxy + def test_retries(self) -> None: # FakeCommand intentionally returns the wrong type. options1, args1 = cast(