Repository: pypa/pip
Pull request #11492 (2022-10-06 12:30:33)

Raise RequirementsFileParseError when missing closing quotation
shlex.split, used to split options in requirements.txt files, might raise a ValueError when the input string is not well formed. Catch the ValueError and re-raise it as a RequirementsFileParseError instead.

Fix https://github.com/pypa/pip/issues/11491 .


Patch:
diff --git a/news/11358.removal.rst b/news/11358.removal.rst
deleted file mode 100644
index 9767949b4..000000000
--- a/news/11358.removal.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Deprecate ``--install-options`` which forces pip to use the deprecated ``install``
-command of ``setuptools``.
diff --git a/news/11491.bugfix.rst b/news/11491.bugfix.rst
new file mode 100644
index 000000000..a8f53927f
--- /dev/null
+++ b/news/11491.bugfix.rst
@@ -0,0 +1,1 @@
+Raise RequirementsFileParseError when parsing malformed requirements options that can't be sucessfully parsed by shlex.
diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py
index 9ff7a3d07..f09503321 100644
--- a/src/pip/_internal/cli/cmdoptions.py
+++ b/src/pip/_internal/cli/cmdoptions.py
@@ -59,6 +59,31 @@ def make_option_group(group: Dict[str, Any], parser: ConfigOptionParser) -> Opti
     return option_group
 
 
+def check_install_build_global(
+    options: Values, check_options: Optional[Values] = None
+) -> None:
+    """Disable wheels if per-setup.py call options are set.
+
+    :param options: The OptionParser options to update.
+    :param check_options: The options to check, if not supplied defaults to
+        options.
+    """
+    if check_options is None:
+        check_options = options
+
+    def getname(n: str) -> Optional[Any]:
+        return getattr(check_options, n, None)
+
+    names = ["build_options", "global_options", "install_options"]
+    if any(map(getname, names)):
+        control = options.format_control
+        control.disallow_binaries()
+        logger.warning(
+            "Disabling all use of wheels due to the use of --build-option "
+            "/ --global-option / --install-option.",
+        )
+
+
 def check_dist_restriction(options: Values, check_target: bool = False) -> None:
     """Function for determining if custom platform options are allowed.
 
diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py
index 4132e0898..26a5080c7 100644
--- a/src/pip/_internal/commands/download.py
+++ b/src/pip/_internal/commands/download.py
@@ -8,10 +8,6 @@ from pip._internal.cli.cmdoptions import make_target_python
 from pip._internal.cli.req_command import RequirementCommand, with_cleanup
 from pip._internal.cli.status_codes import SUCCESS
 from pip._internal.operations.build.build_tracker import get_build_tracker
-from pip._internal.req.req_install import (
-    LegacySetupPyOptionsCheckMode,
-    check_legacy_setup_py_options,
-)
 from pip._internal.utils.misc import ensure_dir, normalize_path, write_output
 from pip._internal.utils.temp_dir import TempDirectory
 
@@ -109,9 +105,6 @@ class DownloadCommand(RequirementCommand):
         )
 
         reqs = self.get_requirements(args, options, finder, session)
-        check_legacy_setup_py_options(
-            options, reqs, LegacySetupPyOptionsCheckMode.DOWNLOAD
-        )
 
         preparer = self.make_requirement_preparer(
             temp_build_dir=directory,
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index e081c27d2..b37303caa 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -27,11 +27,7 @@ from pip._internal.models.installation_report import InstallationReport
 from pip._internal.operations.build.build_tracker import get_build_tracker
 from pip._internal.operations.check import ConflictDetails, check_install_conflicts
 from pip._internal.req import install_given_reqs
-from pip._internal.req.req_install import (
-    InstallRequirement,
-    LegacySetupPyOptionsCheckMode,
-    check_legacy_setup_py_options,
-)
+from pip._internal.req.req_install import InstallRequirement
 from pip._internal.utils.compat import WINDOWS
 from pip._internal.utils.deprecation import (
     LegacyInstallReasonFailedBdistWheel,
@@ -284,6 +280,7 @@ class InstallCommand(RequirementCommand):
         if options.use_user_site and options.target_dir is not None:
             raise CommandError("Can not combine '--user' and '--target'")
 
+        cmdoptions.check_install_build_global(options)
         upgrade_strategy = "to-satisfy-only"
         if options.upgrade:
             upgrade_strategy = options.upgrade_strategy
@@ -342,9 +339,6 @@ class InstallCommand(RequirementCommand):
 
         try:
             reqs = self.get_requirements(args, options, finder, session)
-            check_legacy_setup_py_options(
-                options, reqs, LegacySetupPyOptionsCheckMode.INSTALL
-            )
 
             if "no-binary-enable-wheel-cache" in options.features_enabled:
                 # TODO: remove format_control from WheelCache when the deprecation cycle
@@ -452,7 +446,7 @@ class InstallCommand(RequirementCommand):
                 wheel_cache=wheel_cache,
                 verify=True,
                 build_options=[],
-                global_options=global_options,
+                global_options=[],
             )
 
             # If we're using PEP 517, we cannot do a legacy setup.py install
diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py
index 1afbd562c..5ddb3bd6c 100644
--- a/src/pip/_internal/commands/wheel.py
+++ b/src/pip/_internal/commands/wheel.py
@@ -10,11 +10,7 @@ from pip._internal.cli.req_command import RequirementCommand, with_cleanup
 from pip._internal.cli.status_codes import SUCCESS
 from pip._internal.exceptions import CommandError
 from pip._internal.operations.build.build_tracker import get_build_tracker
-from pip._internal.req.req_install import (
-    InstallRequirement,
-    LegacySetupPyOptionsCheckMode,
-    check_legacy_setup_py_options,
-)
+from pip._internal.req.req_install import InstallRequirement
 from pip._internal.utils.deprecation import deprecated
 from pip._internal.utils.misc import ensure_dir, normalize_path
 from pip._internal.utils.temp_dir import TempDirectory
@@ -105,6 +101,8 @@ class WheelCommand(RequirementCommand):
 
     @with_cleanup
     def run(self, options: Values, args: List[str]) -> int:
+        cmdoptions.check_install_build_global(options)
+
         session = self.get_default_session(options)
 
         finder = self._build_package_finder(options, session)
@@ -122,9 +120,6 @@ class WheelCommand(RequirementCommand):
         )
 
         reqs = self.get_requirements(args, options, finder, session)
-        check_legacy_setup_py_options(
-            options, reqs, LegacySetupPyOptionsCheckMode.WHEEL
-        )
 
         if "no-binary-enable-wheel-cache" in options.features_enabled:
             # TODO: remove format_control from WheelCache when the deprecation cycle
diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py
index e4b4a9962..1f0a277e1 100644
--- a/src/pip/_internal/req/req_file.py
+++ b/src/pip/_internal/req/req_file.py
@@ -186,6 +186,10 @@ def handle_requirement_line(
             constraint=line.constraint,
         )
     else:
+        if options:
+            # Disable wheels if the user has specified build options
+            cmdoptions.check_install_build_global(options, line.opts)
+
         # get the options that apply to requirements
         req_options = {}
         for dest in SUPPORTED_OPTIONS_REQ_DEST:
@@ -393,7 +397,12 @@ def get_line_parser(finder: Optional["PackageFinder"]) -> LineParser:
 
         args_str, options_str = break_args_options(line)
 
-        opts, _ = parser.parse_args(shlex.split(options_str), defaults)
+        try:
+            options = shlex.split(options_str)
+        except ValueError as e:
+            raise OptionParsingError(f"Could not split options: {options_str}") from e
+
+        opts, _ = parser.parse_args(options, defaults)
 
         return args_str, opts
 
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
index 5f29261c2..88d481dfe 100644
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -8,8 +8,6 @@ import shutil
 import sys
 import uuid
 import zipfile
-from enum import Enum
-from optparse import Values
 from typing import Any, Collection, Dict, Iterable, List, Optional, Sequence, Union
 
 from pip._vendor.packaging.markers import Marker
@@ -878,65 +876,3 @@ def check_invalid_constraint_type(req: InstallRequirement) -> str:
         )
 
     return problem
-
-
-def _has_option(options: Values, reqs: List[InstallRequirement], option: str) -> bool:
-    if getattr(options, option, None):
-        return True
-    for req in reqs:
-        if getattr(req, option, None):
-            return True
-    return False
-
-
-def _install_option_ignored(
-    install_options: List[str], reqs: List[InstallRequirement]
-) -> bool:
-    for req in reqs:
-        if (install_options or req.install_options) and not req.use_pep517:
-            return False
-    return True
-
-
-class LegacySetupPyOptionsCheckMode(Enum):
-    INSTALL = 1
-    WHEEL = 2
-    DOWNLOAD = 3
-
-
-def check_legacy_setup_py_options(
-    options: Values,
-    reqs: List[InstallRequirement],
-    mode: LegacySetupPyOptionsCheckMode,
-) -> None:
-    has_install_options = _has_option(options, reqs, "install_options")
-    has_build_options = _has_option(options, reqs, "build_options")
-    has_global_options = _has_option(options, reqs, "global_options")
-    legacy_setup_py_options_present = (
-        has_install_options or has_build_options or has_global_options
-    )
-    if not legacy_setup_py_options_present:
-        return
-
-    options.format_control.disallow_binaries()
-    logger.warning(
-        "Implying --no-binary=:all: due to the presence of "
-        "--build-option / --global-option / --install-option. "
-        "Consider using --config-settings for more flexibility.",
-    )
-    if mode == LegacySetupPyOptionsCheckMode.INSTALL and has_install_options:
-        if _install_option_ignored(options.install_options, reqs):
-            logger.warning(
-                "Ignoring --install-option when building using PEP 517",
-            )
-        else:
-            deprecated(
-                reason=(
-                    "--install-option is deprecated because "
-                    "it forces pip to use the 'setup.py install' "
-                    "command which is itself deprecated."
-                ),
-                issue=11358,
-                replacement="to use --config-settings",
-                gone_in="23.1",
-            )


Test patch:
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
index f4e0a53be..f4f8d4efb 100644
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -842,8 +842,6 @@ def test_install_global_option(script: PipTestEnvironment) -> None:
     )
     assert "INITools==0.1\n" in result.stdout
     assert not result.files_created
-    assert "Implying --no-binary=:all:" in result.stderr
-    assert "Consider using --config-settings" in result.stderr
 
 
 def test_install_with_hacked_egg_info(
diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py
index 19c526aab..c5fc11dd7 100644
--- a/tests/functional/test_install_reqs.py
+++ b/tests/functional/test_install_reqs.py
@@ -342,7 +342,7 @@ def test_install_option_in_requirements_file_overrides_cli(
     reqs_file = script.scratch_path.joinpath("reqs.txt")
     reqs_file.write_text("simple --install-option='-O0'")
 
-    result = script.pip(
+    script.pip(
         "install",
         "--no-index",
         "-f",
@@ -355,9 +355,6 @@ def test_install_option_in_requirements_file_overrides_cli(
     simple_args = simple_sdist.args()
     assert "install" in simple_args
     assert simple_args.index("-O1") < simple_args.index("-O0")
-    assert "Implying --no-binary=:all:" in result.stderr
-    assert "Consider using --config-settings" in result.stderr
-    assert "--install-option is deprecated" in result.stderr
 
 
 def test_constraints_not_installed_by_default(
diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py
index 02b1ba230..76fac40ad 100644
--- a/tests/unit/test_req_file.py
+++ b/tests/unit/test_req_file.py
@@ -786,6 +786,20 @@ class TestParseRequirements:
 
         assert not reqs
 
+    def test_invalid_options(self, tmpdir: Path, finder: PackageFinder) -> None:
+        """
+        Test parsing invalid options such as missing closing quotation
+        """
+        with open(tmpdir.joinpath("req1.txt"), "w") as fp:
+            fp.write("--'data\n")
+
+        with pytest.raises(RequirementsFileParseError):
+            list(
+                parse_reqfile(
+                    tmpdir.joinpath("req1.txt"), finder=finder, session=PipSession()
+                )
+            )
+
     def test_req_file_parse_comment_end_of_line_with_url(
         self, tmpdir: Path, finder: PackageFinder
     ) -> None:
@@ -881,3 +895,5 @@ class TestParseRequirements:
                 < args.index("install")
                 < args.index(install_option)
             )
+        assert options.format_control.no_binary == {":all:"}
+        assert options.format_control.only_binary == set()