Repository: pypa/pip

Issue:
Pip accepts requirements with extras that don't conform to PEP 508
**Description**
See #8285. The form `setuptools_scm>=3.5[toml]` does not conform to [PEP 508](https://www.python.org/dev/peps/pep-0508/#grammar). Pip accepts it, presumably for historical reasons.

We should deprecate and ultimately reject this form.

**Expected behavior**
Pip should only accept standard (PEP 508) formats for requirements.

**How to Reproduce**
See the linked issue.


Discussion:

Resolving patch:
diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py
--- a/src/pip/_internal/req/constructors.py
+++ b/src/pip/_internal/req/constructors.py
@@ -23,6 +23,7 @@
 from pip._internal.models.wheel import Wheel
 from pip._internal.pyproject import make_pyproject_path
 from pip._internal.req.req_install import InstallRequirement
+from pip._internal.utils.deprecation import deprecated
 from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS
 from pip._internal.utils.misc import is_installable_dir, splitext
 from pip._internal.utils.typing import MYPY_CHECK_RUNNING
@@ -370,6 +371,17 @@ def with_source(text):
             if add_msg:
                 msg += '\nHint: {}'.format(add_msg)
             raise InstallationError(msg)
+        else:
+            # Deprecate extras after specifiers: "name>=1.0[extras]"
+            # This currently works by accident because _strip_extras() parses
+            # any extras in the end of the string and those are saved in
+            # RequirementParts
+            for spec in req.specifier:
+                spec_str = str(spec)
+                if spec_str.endswith(']'):
+                    msg = "Extras after version '{}'.".format(spec_str)
+                    replace = "moving the extras before version specifiers"
+                    deprecated(msg, replacement=replace, gone_in="21.0")
     else:
         req = None
 


Test patch:
diff --git a/tests/functional/test_install_extras.py b/tests/functional/test_install_extras.py
--- a/tests/functional/test_install_extras.py
+++ b/tests/functional/test_install_extras.py
@@ -104,6 +104,27 @@ def test_nonexistent_options_listed_in_order(script, data):
     assert matches == ['nonexistent', 'nope']
 
 
+def test_install_deprecated_extra(script, data):
+    """
+    Warn about deprecated order of specifiers and extras.
+
+    Test uses a requirements file to avoid a testing issue where
+    the specifier gets interpreted as shell redirect.
+    """
+    script.scratch_path.joinpath("requirements.txt").write_text(
+        "requires_simple_extra>=0.1[extra]"
+    )
+    simple = script.site_packages / 'simple'
+
+    result = script.pip(
+        'install', '--no-index', '--find-links=' + data.find_links,
+        '-r', script.scratch_path / 'requirements.txt', expect_stderr=True,
+    )
+
+    result.did_create(simple)
+    assert ("DEPRECATION: Extras after version" in result.stderr)
+
+
 def test_install_special_extra(script):
     # Check that uppercase letters and '-' are dealt with
     # make a dummy project
diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py
--- a/tests/functional/test_new_resolver.py
+++ b/tests/functional/test_new_resolver.py
@@ -200,8 +200,6 @@ def test_new_resolver_ignore_dependencies(script):
     [
         "base[add]",
         "base[add] >= 0.1.0",
-        # Non-standard syntax. To deprecate, see pypa/pip#8288.
-        "base >= 0.1.0[add]",
     ],
 )
 def test_new_resolver_installs_extras(tmpdir, script, root_dep):
@@ -228,6 +226,32 @@ def test_new_resolver_installs_extras(tmpdir, script, root_dep):
     assert_installed(script, base="0.1.0", dep="0.1.0")
 
 
+def test_new_resolver_installs_extras_deprecated(tmpdir, script):
+    req_file = tmpdir.joinpath("requirements.txt")
+    req_file.write_text("base >= 0.1.0[add]")
+
+    create_basic_wheel_for_package(
+        script,
+        "base",
+        "0.1.0",
+        extras={"add": ["dep"]},
+    )
+    create_basic_wheel_for_package(
+        script,
+        "dep",
+        "0.1.0",
+    )
+    result = script.pip(
+        "install", "--use-feature=2020-resolver",
+        "--no-cache-dir", "--no-index",
+        "--find-links", script.scratch_path,
+        "-r", req_file,
+        expect_stderr=True
+    )
+    assert "DEPRECATION: Extras after version" in result.stderr
+    assert_installed(script, base="0.1.0", dep="0.1.0")
+
+
 def test_new_resolver_installs_extras_warn_missing(script):
     create_basic_wheel_for_package(
         script,