From e9d6e63854c1c0f72ec58b038951b3c9650ad0e7 Mon Sep 17 00:00:00 2001 From: q0w <43147888+q0w@users.noreply.github.com> Date: Sat, 7 Jan 2023 02:26:25 +0300 Subject: [PATCH] Handle properly pip --no-binary / --only-binary options in requirements.txt format files. (#2834) Fixes https://github.com/tox-dev/tox/issues/2814 --- docs/changelog/2814.bugfix.rst | 1 + src/tox/tox_env/python/pip/req/args.py | 4 +-- src/tox/tox_env/python/pip/req/file.py | 13 ++++++-- src/tox/tox_env/python/pip/req/util.py | 20 ++++++++++++ tests/tox_env/python/pip/req/test_file.py | 39 +++++++++++++++++++---- 5 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 docs/changelog/2814.bugfix.rst diff --git a/docs/changelog/2814.bugfix.rst b/docs/changelog/2814.bugfix.rst new file mode 100644 index 000000000..40d2f9c05 --- /dev/null +++ b/docs/changelog/2814.bugfix.rst @@ -0,0 +1 @@ +Handle properly pip ``--no-binary`` / ``--only-binary`` options in requirements.txt format files. diff --git a/src/tox/tox_env/python/pip/req/args.py b/src/tox/tox_env/python/pip/req/args.py index 23f98ec31..3fd1f6779 100644 --- a/src/tox/tox_env/python/pip/req/args.py +++ b/src/tox/tox_env/python/pip/req/args.py @@ -33,8 +33,8 @@ def _global_options(parser: ArgumentParser) -> None: parser.add_argument("-r", "--requirement", action=AddUniqueAction, dest="requirements") parser.add_argument("-e", "--editable", action=AddUniqueAction, dest="editables") parser.add_argument("-f", "--find-links", action=AddUniqueAction) - parser.add_argument("--no-binary", choices=[":all:", ":none:"]) # TODO: colon separated package names - parser.add_argument("--only-binary", choices=[":all:", ":none:"]) # TODO: colon separated package names + parser.add_argument("--no-binary") + parser.add_argument("--only-binary") parser.add_argument("--prefer-binary", action="store_true", default=False) parser.add_argument("--require-hashes", action="store_true", default=False) parser.add_argument("--pre", action="store_true", default=False) diff --git a/src/tox/tox_env/python/pip/req/file.py b/src/tox/tox_env/python/pip/req/file.py index d8b295589..495ef194f 100644 --- a/src/tox/tox_env/python/pip/req/file.py +++ b/src/tox/tox_env/python/pip/req/file.py @@ -15,7 +15,7 @@ from packaging.requirements import InvalidRequirement, Requirement from .args import build_parser -from .util import VCS, get_url_scheme, is_url, url_to_path +from .util import VCS, get_url_scheme, handle_binary_option, is_url, url_to_path # Matches environment variable-style values in '${MY_VARIABLE_1}' with the variable name consisting of only uppercase # letters, digits or the '_' (underscore). This follows the POSIX standard defined in IEEE Std 1003.1, 2013 Edition. @@ -341,10 +341,17 @@ def _merge_option_line(self, base_opt: Namespace, opt: Namespace, filename: str) base_opt.trusted_hosts = [] if host not in base_opt.trusted_hosts: base_opt.trusted_hosts.append(host) + + no_binary = base_opt.no_binary if hasattr(base_opt, "no_binary") else set() + only_binary = base_opt.only_binary if hasattr(base_opt, "only_binary") else set() if opt.no_binary: - base_opt.no_binary = opt.no_binary + handle_binary_option(opt.no_binary, no_binary, only_binary) if opt.only_binary: - base_opt.only_binary = opt.only_binary + handle_binary_option(opt.only_binary, only_binary, no_binary) + if no_binary: + base_opt.no_binary = no_binary + if only_binary: + base_opt.only_binary = only_binary @staticmethod def _break_args_options(line: str) -> tuple[str, str]: diff --git a/src/tox/tox_env/python/pip/req/util.py b/src/tox/tox_env/python/pip/req/util.py index 9914d1111..2a110a1d8 100644 --- a/src/tox/tox_env/python/pip/req/util.py +++ b/src/tox/tox_env/python/pip/req/util.py @@ -4,6 +4,8 @@ from urllib.parse import urlsplit from urllib.request import url2pathname +from packaging.utils import canonicalize_name + VCS = ["ftp", "ssh", "git", "hg", "bzr", "sftp", "svn"] VALID_SCHEMAS = ["http", "https", "file"] + VCS @@ -26,3 +28,21 @@ def url_to_path(url: str) -> str: raise ValueError(f"non-local file URIs are not supported on this platform: {url!r}") path = url2pathname(netloc + path) return path + + +def handle_binary_option(value: str, target: set[str], other: set[str]) -> None: + new = value.split(",") + while ":all:" in new: + other.clear() + target.clear() + target.add(":all:") + del new[: new.index(":all:") + 1] + if ":none:" not in new: + return + for name in new: + if name == ":none:": + target.clear() + continue + name = canonicalize_name(name) + other.discard(name) + target.add(name) diff --git a/tests/tox_env/python/pip/req/test_file.py b/tests/tox_env/python/pip/req/test_file.py index 9e4e51a2c..d947510c7 100644 --- a/tests/tox_env/python/pip/req/test_file.py +++ b/tests/tox_env/python/pip/req/test_file.py @@ -140,16 +140,43 @@ ["--use-feature", "2020-resolver", "--use-feature", "fast-deps"], id="use-feature multiple duplicate different line", ), - pytest.param("--no-binary :all:", {"no_binary": ":all:"}, [], ["--no-binary", ":all:"], id="no-binary all"), - pytest.param("--no-binary :none:", {"no_binary": ":none:"}, [], ["--no-binary", ":none:"], id="no-binary none"), - pytest.param("--only-binary :all:", {"only_binary": ":all:"}, [], ["--only-binary", ":all:"], id="only-binary all"), + pytest.param("--no-binary :all:", {"no_binary": {":all:"}}, [], ["--no-binary", {":all:"}], id="no-binary all"), + pytest.param("--no-binary :none:", {"no_binary": {":none:"}}, [], [], id="no-binary none"), + pytest.param( + "--only-binary :all:", + {"only_binary": {":all:"}}, + [], + ["--only-binary", {":all:"}], + id="only-binary all", + ), pytest.param( "--only-binary :none:", - {"only_binary": ":none:"}, + {"only_binary": {":none:"}}, + [], [], - ["--only-binary", ":none:"], id="only-binary none", ), + pytest.param( + "--no-binary=foo --only-binary=foo", + {"only_binary": {"foo"}}, + [], + ["--only-binary", {"foo"}], + id="no-binary-and-only-binary", + ), + pytest.param( + "--no-binary=foo --no-binary=:none:", + {}, + [], + [], + id="no-binary-none-last", + ), + pytest.param( + "--only-binary=:none: --no-binary=foo", + {"no_binary": {"foo"}}, + [], + ["--no-binary", {"foo"}], + id="no-binary-none-first", + ), pytest.param("####### example-requirements.txt #######", {}, [], [], id="comment"), pytest.param("\t##### Requirements without Version Specifiers ######", {}, [], [], id="tab and comment"), pytest.param(" # start", {}, [], [], id="space and comment"), @@ -289,7 +316,7 @@ def test_req_file(tmp_path: Path, req: str, opts: dict[str, Any], requirements: req_file = RequirementsFile(requirements_txt, constraint=False) assert req_file.as_root_args == as_args assert str(req_file) == f"-r {requirements_txt}" - assert vars(req_file.options) == opts + assert vars(req_file.options) == (opts if {":none:"} not in opts.values() else {}) found = [str(i) for i in req_file.requirements] assert found == requirements