pip/tests/unit/test_options.py

657 lines
25 KiB
Python

import os
from contextlib import contextmanager
from optparse import Values
from tempfile import NamedTemporaryFile
from typing import Any, Dict, Iterator, List, Tuple, Type, Union, cast
import pytest
import pip._internal.configuration
from pip._internal.cli.main import main
from pip._internal.commands import create_command
from pip._internal.commands.configuration import ConfigurationCommand
from pip._internal.exceptions import PipError
from tests.lib.options_helpers import AddFakeCommandMixin
@contextmanager
def assert_option_error(
capsys: pytest.CaptureFixture[str], expected: str
) -> Iterator[None]:
"""
Assert that a SystemExit occurred because of a parsing error.
Args:
expected: an expected substring of stderr.
"""
with pytest.raises(SystemExit) as excinfo:
yield
assert excinfo.value.code == 2
stderr = capsys.readouterr().err
assert expected in stderr
def assert_is_default_cache_dir(value: str) -> None:
# This path looks different on different platforms, but the path always
# has the substring "pip".
assert "pip" in value
class TestOptionPrecedence(AddFakeCommandMixin):
"""
Tests for confirming our option precedence:
cli -> environment -> subcommand config -> global config -> option
defaults
"""
def get_config_section(self, section: str) -> List[Tuple[str, str]]:
config = {
"global": [("timeout", "-3")],
"fake": [("timeout", "-2")],
}
return config[section]
def get_config_section_global(self, section: str) -> List[Tuple[str, str]]:
config: Dict[str, List[Tuple[str, str]]] = {
"global": [("timeout", "-3")],
"fake": [],
}
return config[section]
def test_env_override_default_int(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""
Test that environment variable overrides an int option default.
"""
monkeypatch.setenv("PIP_TIMEOUT", "-1")
# FakeCommand intentionally returns the wrong type.
options, args = cast(Tuple[Values, List[str]], main(["fake"]))
assert options.timeout == -1
@pytest.mark.parametrize("values", (["F1"], ["F1", "F2"]))
def test_env_override_default_append(
self, values: List[str], monkeypatch: pytest.MonkeyPatch
) -> None:
"""
Test that environment variable overrides an append option default.
"""
monkeypatch.setenv("PIP_FIND_LINKS", " ".join(values))
# FakeCommand intentionally returns the wrong type.
options, args = cast(Tuple[Values, List[str]], main(["fake"]))
assert options.find_links == values
@pytest.mark.parametrize("choices", (["w"], ["s", "w"]))
def test_env_override_default_choice(
self, choices: List[str], monkeypatch: pytest.MonkeyPatch
) -> None:
"""
Test that environment variable overrides a choice option default.
"""
monkeypatch.setenv("PIP_EXISTS_ACTION", " ".join(choices))
# FakeCommand intentionally returns the wrong type.
options, args = cast(Tuple[Values, List[str]], main(["fake"]))
assert options.exists_action == choices
@pytest.mark.parametrize("name", ("PIP_LOG_FILE", "PIP_LOCAL_LOG"))
def test_env_alias_override_default(
self, name: str, monkeypatch: pytest.MonkeyPatch
) -> None:
"""
When an option has multiple long forms, test that the technique of
using the env variable, "PIP_<long form>" works for all cases.
(e.g. PIP_LOG_FILE and PIP_LOCAL_LOG should all work)
"""
monkeypatch.setenv(name, "override.log")
# FakeCommand intentionally returns the wrong type.
options, args = cast(Tuple[Values, List[str]], main(["fake"]))
assert options.log == "override.log"
def test_cli_override_environment(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""
Test the cli overrides and environment variable
"""
monkeypatch.setenv("PIP_TIMEOUT", "-1")
# FakeCommand intentionally returns the wrong type.
options, args = cast(
Tuple[Values, List[str]], main(["fake", "--timeout", "-2"])
)
assert options.timeout == -2
@pytest.mark.parametrize(
"pip_no_cache_dir",
[
# Enabling --no-cache-dir means no cache directory.
"1",
"true",
"on",
"yes",
# For historical / backwards compatibility reasons, we also disable
# the cache directory if provided a value that translates to 0.
"0",
"false",
"off",
"no",
],
)
def test_cache_dir__PIP_NO_CACHE_DIR(
self, pip_no_cache_dir: str, monkeypatch: pytest.MonkeyPatch
) -> None:
"""
Test setting the PIP_NO_CACHE_DIR environment variable without
passing any command-line flags.
"""
monkeypatch.setenv("PIP_NO_CACHE_DIR", pip_no_cache_dir)
# FakeCommand intentionally returns the wrong type.
options, args = cast(Tuple[Values, List[str]], main(["fake"]))
assert options.cache_dir is False
@pytest.mark.parametrize("pip_no_cache_dir", ["yes", "no"])
def test_cache_dir__PIP_NO_CACHE_DIR__with_cache_dir(
self,
pip_no_cache_dir: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
Test setting PIP_NO_CACHE_DIR while also passing an explicit
--cache-dir value.
"""
monkeypatch.setenv("PIP_NO_CACHE_DIR", pip_no_cache_dir)
# FakeCommand intentionally returns the wrong type.
options, args = cast(
Tuple[Values, List[str]], main(["--cache-dir", "/cache/dir", "fake"])
)
# The command-line flag takes precedence.
assert options.cache_dir == "/cache/dir"
@pytest.mark.parametrize("pip_no_cache_dir", ["yes", "no"])
def test_cache_dir__PIP_NO_CACHE_DIR__with_no_cache_dir(
self,
pip_no_cache_dir: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
Test setting PIP_NO_CACHE_DIR while also passing --no-cache-dir.
"""
monkeypatch.setenv("PIP_NO_CACHE_DIR", pip_no_cache_dir)
# FakeCommand intentionally returns the wrong type.
options, args = cast(Tuple[Values, List[str]], main(["--no-cache-dir", "fake"]))
# The command-line flag should take precedence (which has the same
# value in this case).
assert options.cache_dir is False
def test_cache_dir__PIP_NO_CACHE_DIR_invalid__with_no_cache_dir(
self,
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
"""
Test setting PIP_NO_CACHE_DIR to an invalid value while also passing
--no-cache-dir.
"""
monkeypatch.setenv("PIP_NO_CACHE_DIR", "maybe")
expected_err = "--no-cache-dir error: invalid truth value 'maybe'"
with assert_option_error(capsys, expected=expected_err):
main(["--no-cache-dir", "fake"])
class TestUsePEP517Options:
"""
Test options related to using --use-pep517.
"""
def parse_args(self, args: List[str]) -> Values:
# We use DownloadCommand since that is one of the few Command
# classes with the use_pep517 options.
command = create_command("download")
options, args = command.parse_args(args)
return options
def test_no_option(self) -> None:
"""
Test passing no option.
"""
options = self.parse_args([])
assert options.use_pep517 is None
def test_use_pep517(self) -> None:
"""
Test passing --use-pep517.
"""
options = self.parse_args(["--use-pep517"])
assert options.use_pep517 is True
def test_no_use_pep517(self) -> None:
"""
Test passing --no-use-pep517.
"""
options = self.parse_args(["--no-use-pep517"])
assert options.use_pep517 is False
def test_PIP_USE_PEP517_true(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""
Test setting PIP_USE_PEP517 to "true".
"""
monkeypatch.setenv("PIP_USE_PEP517", "true")
options = self.parse_args([])
# This is an int rather than a boolean because strtobool() in pip's
# configuration code returns an int.
assert options.use_pep517 == 1
def test_PIP_USE_PEP517_false(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""
Test setting PIP_USE_PEP517 to "false".
"""
monkeypatch.setenv("PIP_USE_PEP517", "false")
options = self.parse_args([])
# This is an int rather than a boolean because strtobool() in pip's
# configuration code returns an int.
assert options.use_pep517 == 0
def test_use_pep517_and_PIP_USE_PEP517_false(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""
Test passing --use-pep517 and setting PIP_USE_PEP517 to "false".
"""
monkeypatch.setenv("PIP_USE_PEP517", "false")
options = self.parse_args(["--use-pep517"])
assert options.use_pep517 is True
def test_no_use_pep517_and_PIP_USE_PEP517_true(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""
Test passing --no-use-pep517 and setting PIP_USE_PEP517 to "true".
"""
monkeypatch.setenv("PIP_USE_PEP517", "true")
options = self.parse_args(["--no-use-pep517"])
assert options.use_pep517 is False
def test_PIP_NO_USE_PEP517(
self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""
Test setting PIP_NO_USE_PEP517, which isn't allowed.
"""
monkeypatch.setenv("PIP_NO_USE_PEP517", "true")
with assert_option_error(capsys, expected="--no-use-pep517 error"):
self.parse_args([])
class TestOptionsInterspersed(AddFakeCommandMixin):
def test_general_option_after_subcommand(self) -> None:
# FakeCommand intentionally returns the wrong type.
options, args = cast(
Tuple[Values, List[str]], main(["fake", "--timeout", "-1"])
)
assert options.timeout == -1
def test_option_after_subcommand_arg(self) -> None:
# FakeCommand intentionally returns the wrong type.
options, args = cast(
Tuple[Values, List[str]], main(["fake", "arg", "--timeout", "-1"])
)
assert options.timeout == -1
def test_additive_before_after_subcommand(self) -> None:
# FakeCommand intentionally returns the wrong type.
options, args = cast(Tuple[Values, List[str]], main(["-v", "fake", "-v"]))
assert options.verbose == 2
def test_subcommand_option_before_subcommand_fails(self) -> None:
with pytest.raises(SystemExit):
main(["--find-links", "F1", "fake"])
@contextmanager
def tmpconfig(option: str, value: Any, section: str = "global") -> Iterator[str]:
with NamedTemporaryFile(mode="w", delete=False) as f:
f.write(f"[{section}]\n{option}={value}\n")
name = f.name
try:
yield name
finally:
os.unlink(name)
class TestCountOptions(AddFakeCommandMixin):
@pytest.mark.parametrize("option", ("verbose", "quiet"))
@pytest.mark.parametrize("value", range(4))
def test_cli_long(self, option: str, value: int) -> None:
flags = [f"--{option}"] * value
# FakeCommand intentionally returns the wrong type.
opt1, args1 = cast(Tuple[Values, List[str]], main(flags + ["fake"]))
opt2, args2 = cast(Tuple[Values, List[str]], main(["fake"] + flags))
assert getattr(opt1, option) == getattr(opt2, option) == value
@pytest.mark.parametrize("option", ("verbose", "quiet"))
@pytest.mark.parametrize("value", range(1, 4))
def test_cli_short(self, option: str, value: int) -> None:
flag = "-" + option[0] * value
# FakeCommand intentionally returns the wrong type.
opt1, args1 = cast(Tuple[Values, List[str]], main([flag, "fake"]))
opt2, args2 = cast(Tuple[Values, List[str]], main(["fake", flag]))
assert getattr(opt1, option) == getattr(opt2, option) == value
@pytest.mark.parametrize("option", ("verbose", "quiet"))
@pytest.mark.parametrize("value", range(4))
def test_env_var(
self, option: str, value: int, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("PIP_" + option.upper(), str(value))
# FakeCommand intentionally returns the wrong type.
options, args = cast(Tuple[Values, List[str]], main(["fake"]))
assert getattr(options, option) == value
@pytest.mark.parametrize("option", ("verbose", "quiet"))
@pytest.mark.parametrize("value", range(3))
def test_env_var_integrate_cli(
self, option: str, value: int, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("PIP_" + option.upper(), str(value))
# FakeCommand intentionally returns the wrong type.
options, args = cast(Tuple[Values, List[str]], main(["fake", "--" + option]))
assert getattr(options, option) == value + 1
@pytest.mark.parametrize("option", ("verbose", "quiet"))
@pytest.mark.parametrize("value", (-1, "foobar"))
def test_env_var_invalid(
self,
option: str,
value: Any,
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
monkeypatch.setenv("PIP_" + option.upper(), str(value))
with assert_option_error(capsys, expected="a non-negative integer"):
main(["fake"])
# Undocumented, support for backward compatibility
@pytest.mark.parametrize("option", ("verbose", "quiet"))
@pytest.mark.parametrize("value", ("no", "false"))
def test_env_var_false(
self, option: str, value: str, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("PIP_" + option.upper(), str(value))
# FakeCommand intentionally returns the wrong type.
options, args = cast(Tuple[Values, List[str]], main(["fake"]))
assert getattr(options, option) == 0
# Undocumented, support for backward compatibility
@pytest.mark.parametrize("option", ("verbose", "quiet"))
@pytest.mark.parametrize("value", ("yes", "true"))
def test_env_var_true(
self, option: str, value: str, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("PIP_" + option.upper(), str(value))
# FakeCommand intentionally returns the wrong type.
options, args = cast(Tuple[Values, List[str]], main(["fake"]))
assert getattr(options, option) == 1
@pytest.mark.parametrize("option", ("verbose", "quiet"))
@pytest.mark.parametrize("value", range(4))
def test_config_file(
self, option: str, value: int, monkeypatch: pytest.MonkeyPatch
) -> None:
with tmpconfig(option, value) as name:
monkeypatch.setenv("PIP_CONFIG_FILE", name)
# FakeCommand intentionally returns the wrong type.
options, args = cast(Tuple[Values, List[str]], main(["fake"]))
assert getattr(options, option) == value
@pytest.mark.parametrize("option", ("verbose", "quiet"))
@pytest.mark.parametrize("value", range(3))
def test_config_file_integrate_cli(
self, option: str, value: int, monkeypatch: pytest.MonkeyPatch
) -> None:
with tmpconfig(option, value) as name:
monkeypatch.setenv("PIP_CONFIG_FILE", name)
# FakeCommand intentionally returns the wrong type.
options, args = cast(
Tuple[Values, List[str]], main(["fake", "--" + option])
)
assert getattr(options, option) == value + 1
@pytest.mark.parametrize("option", ("verbose", "quiet"))
@pytest.mark.parametrize("value", (-1, "foobar"))
def test_config_file_invalid(
self,
option: str,
value: Any,
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
with tmpconfig(option, value) as name:
monkeypatch.setenv("PIP_CONFIG_FILE", name)
with assert_option_error(capsys, expected="non-negative integer"):
main(["fake"])
# Undocumented, support for backward compatibility
@pytest.mark.parametrize("option", ("verbose", "quiet"))
@pytest.mark.parametrize("value", ("no", "false"))
def test_config_file_false(
self, option: str, value: str, monkeypatch: pytest.MonkeyPatch
) -> None:
with tmpconfig(option, value) as name:
monkeypatch.setenv("PIP_CONFIG_FILE", name)
# FakeCommand intentionally returns the wrong type.
options, args = cast(Tuple[Values, List[str]], main(["fake"]))
assert getattr(options, option) == 0
# Undocumented, support for backward compatibility
@pytest.mark.parametrize("option", ("verbose", "quiet"))
@pytest.mark.parametrize("value", ("yes", "true"))
def test_config_file_true(
self, option: str, value: str, monkeypatch: pytest.MonkeyPatch
) -> None:
with tmpconfig(option, value) as name:
monkeypatch.setenv("PIP_CONFIG_FILE", name)
# FakeCommand intentionally returns the wrong type.
options, args = cast(Tuple[Values, List[str]], main(["fake"]))
assert getattr(options, option) == 1
class TestGeneralOptions(AddFakeCommandMixin):
# the reason to specifically test general options is due to the
# extra processing they receive, and the number of bugs we've had
def test_cache_dir__default(self) -> None:
# FakeCommand intentionally returns the wrong type.
options, args = cast(Tuple[Values, List[str]], main(["fake"]))
# With no options the default cache dir should be used.
assert_is_default_cache_dir(options.cache_dir)
def test_cache_dir__provided(self) -> None:
# FakeCommand intentionally returns the wrong type.
options, args = cast(
Tuple[Values, List[str]], main(["--cache-dir", "/cache/dir", "fake"])
)
assert options.cache_dir == "/cache/dir"
def test_no_cache_dir__provided(self) -> None:
# FakeCommand intentionally returns the wrong type.
options, args = cast(Tuple[Values, List[str]], main(["--no-cache-dir", "fake"]))
assert options.cache_dir is False
def test_require_virtualenv(self) -> None:
# FakeCommand intentionally returns the wrong type.
options1, args1 = cast(
Tuple[Values, List[str]], main(["--require-virtualenv", "fake"])
)
options2, args2 = cast(
Tuple[Values, List[str]], main(["fake", "--require-virtualenv"])
)
assert options1.require_venv
assert options2.require_venv
def test_log(self) -> None:
# FakeCommand intentionally returns the wrong type.
options1, args1 = cast(
Tuple[Values, List[str]], main(["--log", "path", "fake"])
)
options2, args2 = cast(
Tuple[Values, List[str]], main(["fake", "--log", "path"])
)
assert options1.log == options2.log == "path"
def test_local_log(self) -> None:
# FakeCommand intentionally returns the wrong type.
options1, args1 = cast(
Tuple[Values, List[str]], main(["--local-log", "path", "fake"])
)
options2, args2 = cast(
Tuple[Values, List[str]], main(["fake", "--local-log", "path"])
)
assert options1.log == options2.log == "path"
def test_no_input(self) -> None:
# FakeCommand intentionally returns the wrong type.
options1, args1 = cast(Tuple[Values, List[str]], main(["--no-input", "fake"]))
options2, args2 = cast(Tuple[Values, List[str]], main(["fake", "--no-input"]))
assert options1.no_input
assert options2.no_input
def test_proxy(self) -> None:
# FakeCommand intentionally returns the wrong type.
options1, args1 = cast(
Tuple[Values, List[str]], main(["--proxy", "path", "fake"])
)
options2, args2 = cast(
Tuple[Values, List[str]], main(["fake", "--proxy", "path"])
)
assert options1.proxy == options2.proxy == "path"
def test_retries(self) -> None:
# FakeCommand intentionally returns the wrong type.
options1, args1 = cast(
Tuple[Values, List[str]], main(["--retries", "-1", "fake"])
)
options2, args2 = cast(
Tuple[Values, List[str]], main(["fake", "--retries", "-1"])
)
assert options1.retries == options2.retries == -1
def test_timeout(self) -> None:
# FakeCommand intentionally returns the wrong type.
options1, args1 = cast(
Tuple[Values, List[str]], main(["--timeout", "-1", "fake"])
)
options2, args2 = cast(
Tuple[Values, List[str]], main(["fake", "--timeout", "-1"])
)
assert options1.timeout == options2.timeout == -1
def test_exists_action(self) -> None:
# FakeCommand intentionally returns the wrong type.
options1, args1 = cast(
Tuple[Values, List[str]], main(["--exists-action", "w", "fake"])
)
options2, args2 = cast(
Tuple[Values, List[str]], main(["fake", "--exists-action", "w"])
)
assert options1.exists_action == options2.exists_action == ["w"]
def test_cert(self) -> None:
# FakeCommand intentionally returns the wrong type.
options1, args1 = cast(
Tuple[Values, List[str]], main(["--cert", "path", "fake"])
)
options2, args2 = cast(
Tuple[Values, List[str]], main(["fake", "--cert", "path"])
)
assert options1.cert == options2.cert == "path"
def test_client_cert(self) -> None:
# FakeCommand intentionally returns the wrong type.
options1, args1 = cast(
Tuple[Values, List[str]], main(["--client-cert", "path", "fake"])
)
options2, args2 = cast(
Tuple[Values, List[str]], main(["fake", "--client-cert", "path"])
)
assert options1.client_cert == options2.client_cert == "path"
class TestOptionsConfigFiles:
def test_venv_config_file_found(self, monkeypatch: pytest.MonkeyPatch) -> None:
# strict limit on the global config files list
monkeypatch.setattr(
pip._internal.utils.appdirs, "site_config_dirs", lambda _: ["/a/place"]
)
cp = pip._internal.configuration.Configuration(isolated=False)
files = []
for _, val in cp.iter_config_files():
files.extend(val)
assert len(files) == 4
@pytest.mark.parametrize(
"args, expect",
(
([], None),
(["--global"], "global"),
(["--site"], "site"),
(["--user"], "user"),
(["--global", "--user"], PipError),
(["--global", "--site"], PipError),
(["--global", "--site", "--user"], PipError),
),
)
def test_config_file_options(
self,
monkeypatch: pytest.MonkeyPatch,
args: List[str],
expect: Union[None, str, Type[PipError]],
) -> None:
cmd = cast(ConfigurationCommand, create_command("config"))
# Replace a handler with a no-op to avoid side effects
monkeypatch.setattr(cmd, "get_name", lambda *a: None)
options, args = cmd.parser.parse_args(args + ["get", "name"])
if expect is PipError:
with pytest.raises(PipError):
cmd._determine_file(options, need_value=False)
else:
assert expect == cmd._determine_file(options, need_value=False)
class TestOptionsExpandUser(AddFakeCommandMixin):
def test_cache_dir(self) -> None:
# FakeCommand intentionally returns the wrong type.
options, args = cast(
Tuple[Values, List[str]], main(["--cache-dir", "~/cache/dir", "fake"])
)
assert options.cache_dir == os.path.expanduser("~/cache/dir")
def test_log(self) -> None:
# FakeCommand intentionally returns the wrong type.
options, args = cast(
Tuple[Values, List[str]], main(["--log", "~/path", "fake"])
)
assert options.log == os.path.expanduser("~/path")
def test_local_log(self) -> None:
# FakeCommand intentionally returns the wrong type.
options, args = cast(
Tuple[Values, List[str]], main(["--local-log", "~/path", "fake"])
)
assert options.log == os.path.expanduser("~/path")
def test_cert(self) -> None:
# FakeCommand intentionally returns the wrong type.
options, args = cast(
Tuple[Values, List[str]], main(["--cert", "~/path", "fake"])
)
assert options.cert == os.path.expanduser("~/path")
def test_client_cert(self) -> None:
# FakeCommand intentionally returns the wrong type.
options, args = cast(
Tuple[Values, List[str]], main(["--client-cert", "~/path", "fake"])
)
assert options.client_cert == os.path.expanduser("~/path")