Repository: pypa/pip

Issue:
Allow --prefer-binary option in requirements file
* Pip version: 20.0.2

It would be very nice to be able to specify the `--prefer-binary` option in a requirements file.

Per https://pip.pypa.io/en/stable/reference/pip_install/#requirements-file-format, both `--no-binary` and  `--only-binary` are supported, but not `--prefer-binary`. I tried it out to make sure the documentation was accurate, and it is.
```
ERROR: Invalid requirement: --prefer-binary
pip3: error: no such option: --prefer-binary
```


Discussion:
Hi @pradyunsg 

Can I take up this issue and file a PR?

I was looking at [src.pip._internal.req.req_file.SUPPORTED_OPTIONS](https://github.com/pypa/pip/blob/master/src/pip/_internal/req/req_file.py#L54) and saw that the `prefix_binary` cmdoptions is missing. I think adding it back should fix this PR. 

Also I didn't find good examples on how can the `--xx-binary` options be used in a requirements file, so  I can add it to perhaps as part of that PR.
@deveshks sounds about right, and separate PRs for the semi-related documentation update would be a good idea. :)

Resolving patch:
diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py
--- a/src/pip/_internal/build_env.py
+++ b/src/pip/_internal/build_env.py
@@ -193,6 +193,8 @@ def install_requirements(
             args.extend(['--trusted-host', host])
         if finder.allow_all_prereleases:
             args.append('--pre')
+        if finder.prefer_binary:
+            args.append('--prefer-binary')
         args.append('--')
         args.extend(requirements)
         with open_spinner(message) as spinner:
diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py
--- a/src/pip/_internal/index/package_finder.py
+++ b/src/pip/_internal/index/package_finder.py
@@ -693,6 +693,15 @@ def set_allow_all_prereleases(self):
         # type: () -> None
         self._candidate_prefs.allow_all_prereleases = True
 
+    @property
+    def prefer_binary(self):
+        # type: () -> bool
+        return self._candidate_prefs.prefer_binary
+
+    def set_prefer_binary(self):
+        # type: () -> None
+        self._candidate_prefs.prefer_binary = True
+
     def make_link_evaluator(self, project_name):
         # type: (str) -> LinkEvaluator
         canonical_name = canonicalize_name(project_name)
diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py
--- a/src/pip/_internal/req/req_file.py
+++ b/src/pip/_internal/req/req_file.py
@@ -60,6 +60,7 @@
     cmdoptions.find_links,
     cmdoptions.no_binary,
     cmdoptions.only_binary,
+    cmdoptions.prefer_binary,
     cmdoptions.require_hashes,
     cmdoptions.pre,
     cmdoptions.trusted_host,
@@ -260,6 +261,9 @@ def handle_option_line(
         if opts.pre:
             finder.set_allow_all_prereleases()
 
+        if opts.prefer_binary:
+            finder.set_prefer_binary()
+
         if session:
             for host in opts.trusted_hosts or []:
                 source = 'line {} of {}'.format(lineno, filename)


Test patch:
diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py
--- a/tests/functional/test_download.py
+++ b/tests/functional/test_download.py
@@ -688,6 +688,30 @@ def test_download_prefer_binary_when_tarball_higher_than_wheel(script, data):
     )
 
 
+def test_prefer_binary_tarball_higher_than_wheel_req_file(script, data):
+    fake_wheel(data, 'source-0.8-py2.py3-none-any.whl')
+    script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent("""
+                --prefer-binary
+                 source
+                """))
+    result = script.pip(
+        'download',
+        '-r', script.scratch_path / 'test-req.txt',
+        '--no-index',
+        '-f', data.packages,
+        '-d', '.'
+    )
+
+    assert (
+        Path('scratch') / 'source-0.8-py2.py3-none-any.whl'
+        in result.files_created
+    )
+    assert (
+        Path('scratch') / 'source-1.0.tar.gz'
+        not in result.files_created
+    )
+
+
 def test_download_prefer_binary_when_wheel_doesnt_satisfy_req(script, data):
     fake_wheel(data, 'source-0.8-py2.py3-none-any.whl')
     script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent("""
@@ -712,6 +736,30 @@ def test_download_prefer_binary_when_wheel_doesnt_satisfy_req(script, data):
     )
 
 
+def test_prefer_binary_when_wheel_doesnt_satisfy_req_req_file(script, data):
+    fake_wheel(data, 'source-0.8-py2.py3-none-any.whl')
+    script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent("""
+        --prefer-binary
+        source>0.9
+        """))
+
+    result = script.pip(
+        'download',
+        '--no-index',
+        '-f', data.packages,
+        '-d', '.',
+        '-r', script.scratch_path / 'test-req.txt'
+    )
+    assert (
+        Path('scratch') / 'source-1.0.tar.gz'
+        in result.files_created
+    )
+    assert (
+        Path('scratch') / 'source-0.8-py2.py3-none-any.whl'
+        not in result.files_created
+    )
+
+
 def test_download_prefer_binary_when_only_tarball_exists(script, data):
     result = script.pip(
         'download',
@@ -726,6 +774,24 @@ def test_download_prefer_binary_when_only_tarball_exists(script, data):
     )
 
 
+def test_prefer_binary_when_only_tarball_exists_req_file(script, data):
+    script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent("""
+            --prefer-binary
+            source
+            """))
+    result = script.pip(
+        'download',
+        '--no-index',
+        '-f', data.packages,
+        '-d', '.',
+        '-r', script.scratch_path / 'test-req.txt'
+    )
+    assert (
+        Path('scratch') / 'source-1.0.tar.gz'
+        in result.files_created
+    )
+
+
 @pytest.fixture(scope="session")
 def shared_script(tmpdir_factory, script_factory):
     tmpdir = Path(str(tmpdir_factory.mktemp("download_shared_script")))