Repository: jazzband/pip-tools (python)

Issue:
Output --no-binary and --only-binary options to preserve pip behavior
<!-- Describe the issue briefly here. -->

#### Environment Versions

1. OS Type: Linux
1. Python version: 3.12.3
1. pip version: 24.0
1. pip-tools version: 7.4.1

#### Steps to replicate

1. Compile using `--pip-args='--only-binary=:all: --no-binary=library'
2. See that compiled requirements list the `--no-binary=library` option first, `--only-binary=:all:` option second.
3. When attempting to install from these requirements the `--no-binary` is wiped out by the `--only-binary=:all:`

#### Expected result

The resulting requirements contain `--no-binary` and `--only-binary` options that have the same behavior as input options.

#### Actual result

Requirements don't have the same behavior as input options.

This improvement matters because using --no-binary/--only-binary is the best way for users to control the amount of potential code execution that happens during the installation process. I have a local fix that I plan on creating a PR.


Pull request description:
Fix ordering of format controls to preserve behavior of pip
<!--- Describe the changes here. --->

##### Contributor checklist

- [x] Included tests for the changes.
- [x] PR title is short, clear, and ready to be included in the user-facing changelog.

##### Maintainer checklist

- [ ] Verified one of these labels is present: `backwards incompatible`, `feature`, `enhancement`, `deprecation`, `bug`, `dependency`, `docs` or `skip-changelog` as they determine changelog listing.
- [ ] Assign the PR to an existing or new milestone for the target version (following [Semantic Versioning](https://blog.versioneye.com/2014/01/16/semantic-versioning/)).

Closes #2081 

Patch:
diff --git a/piptools/writer.py b/piptools/writer.py
index bcb70f0..da603f1 100644
--- a/piptools/writer.py
+++ b/piptools/writer.py
@@ -148,9 +148,27 @@ class OutputWriter:
                 yield f"--trusted-host {trusted_host}"
 
     def write_format_controls(self) -> Iterator[str]:
-        for nb in dedup(sorted(self.format_control.no_binary)):
+        # The ordering of output needs to preserve the behavior of pip's
+        # FormatControl.get_allowed_formats(). The behavior is the following:
+        #
+        #   * Parsing of CLI options happens first to last.
+        #   * --only-binary takes precedence over --no-binary
+        #   * Package names take precedence over :all:
+        #   * We'll never see :all: in both due to mutual exclusion.
+        #
+        # So in summary, we want to emit :all: first and then package names later.
+        no_binary = self.format_control.no_binary.copy()
+        only_binary = self.format_control.only_binary.copy()
+
+        if ":all:" in no_binary:
+            yield "--no-binary :all:"
+            no_binary.remove(":all:")
+        if ":all:" in only_binary:
+            yield "--only-binary :all:"
+            only_binary.remove(":all:")
+        for nb in dedup(sorted(no_binary)):
             yield f"--no-binary {nb}"
-        for ob in dedup(sorted(self.format_control.only_binary)):
+        for ob in dedup(sorted(only_binary)):
             yield f"--only-binary {ob}"
 
     def write_find_links(self) -> Iterator[str]:


Test patch:
diff --git a/tests/test_writer.py b/tests/test_writer.py
index 8a59269..dd43544 100644
--- a/tests/test_writer.py
+++ b/tests/test_writer.py
@@ -301,6 +301,41 @@ def test_write_format_controls(writer):
     assert lines == expected_lines
 
 
+@pytest.mark.parametrize(
+    ("no_binary", "only_binary", "expected_lines"),
+    (
+        (
+            [":all:"],
+            ["django"],
+            [
+                "--no-binary :all:",
+                "--only-binary django",
+            ],
+        ),
+        (
+            ["django"],
+            [":all:"],
+            [
+                "--only-binary :all:",
+                "--no-binary django",
+            ],
+        ),
+    ),
+)
+def test_write_format_controls_all(writer, no_binary, only_binary, expected_lines):
+    """
+    Tests --no-binary/--only-binary options
+    with the value of :all:. We want to preserve
+    the FormatControl behavior so we emit :all:
+    first before packages.
+    """
+
+    writer.format_control = FormatControl(no_binary=no_binary, only_binary=only_binary)
+    lines = list(writer.write_format_controls())
+
+    assert lines == expected_lines
+
+
 @pytest.mark.parametrize(
     ("index_urls", "expected_lines"),
     (