Skip to content

Commit 5dcf063

Browse files
authored
Merge pull request #13732 from ichard26/finish-non-bare-egg-fragment-deprecation
Remove support for non-bare egg fragments
2 parents aa2b770 + 0b9c2d3 commit 5dcf063

File tree

9 files changed

+67
-50
lines changed

9 files changed

+67
-50
lines changed

docs/html/topics/vcs-support.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ pip also supports an `egg` fragment to specify the "project name". This is a leg
163163
feature and its use is discouraged in favour of the
164164
{ref}`Direct URL <pypug:dependency-specifiers>` form.
165165
166-
The `egg` fragment **should** be a bare {ref}`project name <pypug:name-normalization>`.
166+
The `egg` fragment **MUST** be a bare {ref}`project name <pypug:name-normalization>`.
167167
Anything else is not guaranteed to work.
168168
```
169169

news/13157.removal.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Remove support for non-bare project names in egg fragments. Affected users should use
2+
the `Direct URL requirement syntax <https://packaging.python.org/en/latest/specifications/version-specifiers/#direct-references>`_.

src/pip/_internal/exceptions.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from pip._vendor.requests.models import PreparedRequest, Request, Response
3232

3333
from pip._internal.metadata import BaseDistribution
34+
from pip._internal.models.link import Link
3435
from pip._internal.network.download import _FileDownload
3536
from pip._internal.req.req_install import InstallRequirement
3637

@@ -899,6 +900,29 @@ def __init__(self, failed: list[InstallRequirement]) -> None:
899900
)
900901

901902

903+
class InvalidEggFragment(DiagnosticPipError):
904+
reference = "invalid-egg-fragment"
905+
906+
def __init__(self, link: Link, fragment: str) -> None:
907+
hint = ""
908+
if ">" in fragment or "=" in fragment or "<" in fragment:
909+
hint = (
910+
"Version specifiers are silently ignored for URL references. "
911+
"Remove them. "
912+
)
913+
if "[" in fragment and "]" in fragment:
914+
hint += "Try using the Direct URL requirement syntax: 'name[extra] @ URL'"
915+
916+
if not hint:
917+
hint = "Egg fragments can only be a valid project name."
918+
919+
super().__init__(
920+
message=f"The '{escape(fragment)}' egg fragment is invalid",
921+
context=f"from '{escape(str(link))}'",
922+
hint_stmt=escape(hint),
923+
)
924+
925+
902926
class BuildDependencyInstallError(DiagnosticPipError):
903927
"""Raised when build dependencies cannot be installed."""
904928

src/pip/_internal/models/link.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
NamedTuple,
1717
)
1818

19+
from pip._internal.exceptions import InvalidEggFragment
1920
from pip._internal.utils.datetime import parse_iso_datetime
20-
from pip._internal.utils.deprecation import deprecated
2121
from pip._internal.utils.filetypes import WHEEL_EXTENSION
2222
from pip._internal.utils.hashes import Hashes
2323
from pip._internal.utils.misc import (
@@ -483,12 +483,7 @@ def _egg_fragment(self) -> str | None:
483483
# an optional extras specifier. Anything else is invalid.
484484
project_name = match.group(1)
485485
if not self._project_name_re.match(project_name):
486-
deprecated(
487-
reason=f"{self} contains an egg fragment with a non-PEP 508 name.",
488-
replacement="to use the req @ url syntax, and remove the egg fragment",
489-
gone_in="26.0",
490-
issue=13157,
491-
)
486+
raise InvalidEggFragment(self, project_name)
492487

493488
return project_name
494489

src/pip/_internal/req/constructors.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,10 @@ def install_req_from_editable(
258258
permit_editable_wheels: bool = False,
259259
config_settings: dict[str, str | list[str]] | None = None,
260260
) -> InstallRequirement:
261-
parts = parse_req_from_editable(editable_req)
261+
if constraint:
262+
raise InstallationError("Editable requirements are not allowed as constraints")
262263

264+
parts = parse_req_from_editable(editable_req)
263265
return InstallRequirement(
264266
parts.requirement,
265267
comes_from=comes_from,

tests/functional/test_install_reqs.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -646,12 +646,12 @@ def test_install_with_extras_from_constraints(
646646
script: PipTestEnvironment, data: TestData, resolver_variant: ResolverVariant
647647
) -> None:
648648
to_install = data.packages.joinpath("LocalExtras")
649-
script.scratch_path.joinpath("constraints.txt").write_text(
650-
f"{to_install.as_uri()}#egg=LocalExtras[bar]"
649+
file = script.temporary_file(
650+
"constraints.txt", f"LocalExtras[bar] @ {to_install.as_uri()}"
651651
)
652652
result = script.pip_install_local(
653653
"-c",
654-
script.scratch_path / "constraints.txt",
654+
file,
655655
"LocalExtras",
656656
allow_stderr_warning=True,
657657
expect_error=(resolver_variant == "resolvelib"),
@@ -684,12 +684,12 @@ def test_install_with_extras_joined(
684684
script: PipTestEnvironment, data: TestData, resolver_variant: ResolverVariant
685685
) -> None:
686686
to_install = data.packages.joinpath("LocalExtras")
687-
script.scratch_path.joinpath("constraints.txt").write_text(
688-
f"{to_install.as_uri()}#egg=LocalExtras[bar]"
687+
file = script.temporary_file(
688+
"constraints.txt", f"LocalExtras[bar] @ {to_install.as_uri()}"
689689
)
690690
result = script.pip_install_local(
691691
"-c",
692-
script.scratch_path / "constraints.txt",
692+
file,
693693
"LocalExtras[baz]",
694694
allow_stderr_warning=True,
695695
expect_error=(resolver_variant == "resolvelib"),
@@ -705,12 +705,12 @@ def test_install_with_extras_editable_joined(
705705
script: PipTestEnvironment, data: TestData, resolver_variant: ResolverVariant
706706
) -> None:
707707
to_install = data.packages.joinpath("LocalExtras")
708-
script.scratch_path.joinpath("constraints.txt").write_text(
709-
f"-e {to_install.as_uri()}#egg=LocalExtras[bar]"
708+
file = script.temporary_file(
709+
"constraints.txt", f"-e LocalExtras[bar] @ {to_install.as_uri()}"
710710
)
711711
result = script.pip_install_local(
712712
"-c",
713-
script.scratch_path / "constraints.txt",
713+
file,
714714
"LocalExtras[baz]",
715715
allow_stderr_warning=True,
716716
expect_error=(resolver_variant == "resolvelib"),

tests/unit/test_link.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44

5+
from pip._internal.exceptions import InvalidEggFragment, PipError
56
from pip._internal.models.link import Link, links_equivalent
67
from pip._internal.utils.hashes import Hashes
78

@@ -80,20 +81,6 @@ def test_fragments(self) -> None:
8081
assert "eggname" == Link(url).egg_fragment
8182
assert "subdir" == Link(url).subdirectory_fragment
8283

83-
# Extras are supported and preserved in the egg fragment,
84-
# even the empty extras specifier.
85-
# This behavior is deprecated and will change in pip 25.
86-
url = "git+https://example.com/package#egg=eggname[extra]"
87-
assert "eggname[extra]" == Link(url).egg_fragment
88-
assert None is Link(url).subdirectory_fragment
89-
url = "git+https://example.com/package#egg=eggname[extra1,extra2]"
90-
assert "eggname[extra1,extra2]" == Link(url).egg_fragment
91-
assert None is Link(url).subdirectory_fragment
92-
url = "git+https://example.com/package#egg=eggname[]"
93-
assert "eggname[]" == Link(url).egg_fragment
94-
assert None is Link(url).subdirectory_fragment
95-
96-
@pytest.mark.xfail(reason="Behavior change scheduled for 25.0", strict=True)
9784
@pytest.mark.parametrize(
9885
"fragment",
9986
[
@@ -102,15 +89,32 @@ def test_fragments(self) -> None:
10289
# Version specifiers are not valid in egg fragments.
10390
"eggname==1.2.3",
10491
"eggname>=1.2.3",
105-
# The extras specifier must be in PEP 508 form.
92+
# Extras are also prohibited.
10693
"eggname[!]",
94+
"eggname[extra]",
95+
"eggname[extra1,extra2]",
96+
"eggmame[]",
97+
"eggname[extra]==1000",
10798
],
10899
)
109100
def test_invalid_egg_fragments(self, fragment: str) -> None:
110101
url = f"git+https://example.com/package#egg={fragment}"
111-
with pytest.raises(ValueError):
102+
with pytest.raises(PipError):
112103
Link(url)
113104

105+
def test_invalid_egg_fragment_with_extras_and_version_hint(self) -> None:
106+
"""Test that fragments with extras and version specifiers get proper hint."""
107+
108+
url = "git+https://example.com/package#egg=eggname[extra]==1.0"
109+
with pytest.raises(InvalidEggFragment) as exc_info:
110+
Link(url)
111+
112+
# The hint should suggest Direct URL syntax, not just "remove version
113+
# specifiers" because the extras require Direct URL syntax anyway.
114+
hint = str(exc_info.value.hint_stmt)
115+
assert r"name\[extra] @ URL" in hint
116+
assert "Version specifiers are silently ignored" in hint
117+
114118
@pytest.mark.parametrize(
115119
"yanked_reason, expected",
116120
[

tests/unit/test_req.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -701,7 +701,7 @@ def test_extras_for_line_path_requirement(self) -> None:
701701
assert req.extras == {"ex1", "ex2"}
702702

703703
def test_extras_for_line_url_requirement(self) -> None:
704-
line = "git+https://url#egg=SomeProject[ex1,ex2]"
704+
line = "SomeProject[ex1,ex2] @ git+https://url"
705705
filename = "filename"
706706
comes_from = f"-r {filename} (line 1)"
707707
req = install_req_from_line(line, comes_from=comes_from)
@@ -717,7 +717,7 @@ def test_extras_for_editable_path_requirement(self) -> None:
717717
assert req.extras == {"ex1", "ex2"}
718718

719719
def test_extras_for_editable_url_requirement(self) -> None:
720-
url = "git+https://url#egg=SomeProject[ex1,ex2]"
720+
url = "SomeProject[ex1,ex2] @ git+https://url"
721721
filename = "filename"
722722
comes_from = f"-r {filename} (line 1)"
723723
req = install_req_from_editable(url, comes_from=comes_from)
@@ -894,10 +894,10 @@ def test_parse_editable_explicit_vcs() -> None:
894894

895895

896896
def test_parse_editable_vcs_extras() -> None:
897-
assert parse_editable("svn+https://foo#egg=foo[extras]") == (
898-
"foo[extras]",
899-
"svn+https://foo#egg=foo[extras]",
900-
set(),
897+
assert parse_editable("foo[extras] @ svn+https://foo") == (
898+
"foo",
899+
"svn+https://foo",
900+
{"extras"},
901901
)
902902

903903

tests/unit/test_req_file.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -325,16 +325,6 @@ def test_yield_editable_requirement(self, line_processor: LineProcessor) -> None
325325
req = install_req_from_editable(url, comes_from=comes_from)
326326
assert repr(line_processor(line, filename, 1)[0]) == repr(req)
327327

328-
def test_yield_editable_constraint(self, line_processor: LineProcessor) -> None:
329-
url = "git+https://url#egg=SomeProject"
330-
line = f"-e {url}"
331-
filename = "filename"
332-
comes_from = f"-c {filename} (line {1})"
333-
req = install_req_from_editable(url, comes_from=comes_from, constraint=True)
334-
found_req = line_processor(line, filename, 1, constraint=True)[0]
335-
assert repr(found_req) == repr(req)
336-
assert found_req.constraint is True
337-
338328
def test_nested_constraints_file(
339329
self, monkeypatch: pytest.MonkeyPatch, tmpdir: Path, session: PipSession
340330
) -> None:

0 commit comments

Comments
 (0)