Skip to content

Commit dca39dd

Browse files
authored
Merge pull request #11617 from trail-of-forks/ww/restrict-egg-fragement
Restrict `#egg=` fragments to valid PEP 508 names
2 parents cecd346 + 64fe223 commit dca39dd

File tree

5 files changed

+63
-5
lines changed

5 files changed

+63
-5
lines changed

docs/html/topics/vcs-support.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,14 @@ option.
139139
pip looks at 2 fragments for VCS URLs:
140140

141141
- `egg`: For specifying the "project name" for use in pip's dependency
142-
resolution logic. eg: `egg=project_name`
142+
resolution logic. e.g.: `egg=project_name`
143+
144+
The `egg` fragment **should** be a bare
145+
[PEP 508](https://peps.python.org/pep-0508/) project name. Anything else
146+
is not guaranteed to work.
147+
143148
- `subdirectory`: For specifying the path to the Python package, when it is not
144-
in the root of the VCS directory. eg: `pkg_dir`
149+
in the root of the VCS directory. e.g.: `pkg_dir`
145150

146151
````{admonition} Example
147152
If your repository layout is:
File renamed without changes.

news/11617.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Deprecated a historical ambiguity in how ``egg`` fragments in URL-style
2+
requirements are formatted and handled. ``egg`` fragments that do not look
3+
like PEP 508 names now produce a deprecation warning.

src/pip/_internal/models/link.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
Union,
1919
)
2020

21+
from pip._internal.utils.deprecation import deprecated
2122
from pip._internal.utils.filetypes import WHEEL_EXTENSION
2223
from pip._internal.utils.hashes import Hashes
2324
from pip._internal.utils.misc import (
@@ -166,6 +167,7 @@ class Link(KeyBasedCompareMixin):
166167
"dist_info_metadata",
167168
"link_hash",
168169
"cache_link_parsing",
170+
"egg_fragment",
169171
]
170172

171173
def __init__(
@@ -229,6 +231,7 @@ def __init__(
229231
super().__init__(key=url, defining_class=Link)
230232

231233
self.cache_link_parsing = cache_link_parsing
234+
self.egg_fragment = self._egg_fragment()
232235

233236
@classmethod
234237
def from_json(
@@ -358,12 +361,28 @@ def url_without_fragment(self) -> str:
358361

359362
_egg_fragment_re = re.compile(r"[#&]egg=([^&]*)")
360363

361-
@property
362-
def egg_fragment(self) -> Optional[str]:
364+
# Per PEP 508.
365+
_project_name_re = re.compile(
366+
r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE
367+
)
368+
369+
def _egg_fragment(self) -> Optional[str]:
363370
match = self._egg_fragment_re.search(self._url)
364371
if not match:
365372
return None
366-
return match.group(1)
373+
374+
# An egg fragment looks like a PEP 508 project name, along with
375+
# an optional extras specifier. Anything else is invalid.
376+
project_name = match.group(1)
377+
if not self._project_name_re.match(project_name):
378+
deprecated(
379+
reason=f"{self} contains an egg fragment with a non-PEP 508 name",
380+
replacement="to use the req @ url syntax, and remove the egg fragment",
381+
gone_in="25.0",
382+
issue=11617,
383+
)
384+
385+
return project_name
367386

368387
_subdirectory_fragment_re = re.compile(r"[#&]subdirectory=([^&]*)")
369388

tests/unit/test_link.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,37 @@ def test_fragments(self) -> None:
8080
assert "eggname" == Link(url).egg_fragment
8181
assert "subdir" == Link(url).subdirectory_fragment
8282

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)
97+
@pytest.mark.parametrize(
98+
"fragment",
99+
[
100+
# Package names in egg fragments must be in PEP 508 form.
101+
"~invalid~package~name~",
102+
# Version specifiers are not valid in egg fragments.
103+
"eggname==1.2.3",
104+
"eggname>=1.2.3",
105+
# The extras specifier must be in PEP 508 form.
106+
"eggname[!]",
107+
],
108+
)
109+
def test_invalid_egg_fragments(self, fragment: str) -> None:
110+
url = f"git+https://example.com/package#egg={fragment}"
111+
with pytest.raises(Exception):
112+
Link(url)
113+
83114
@pytest.mark.parametrize(
84115
"yanked_reason, expected",
85116
[

0 commit comments

Comments
 (0)