Skip to content

Commit 4164131

Browse files
committed
Support Direct URL editable requirements
This is crucial for the deprecation of non-bare egg URL fragments as they used to be the sole way to request an extra for a VCS editable install. I've also removed all references to egg fragments from the user docs and added further explanation of the Direct URL form in the VCS support topic.
1 parent a4b40f6 commit 4164131

File tree

8 files changed

+159
-37
lines changed

8 files changed

+159
-37
lines changed

docs/html/cli/pip_install.rst

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ for the name and project version (this is in theory slightly less reliable
6666
than using the ``egg_info`` command, but avoids downloading and processing
6767
unnecessary numbers of files).
6868

69-
Any URL may use the ``#egg=name`` syntax (see :doc:`../topics/vcs-support`) to
70-
explicitly state the project name.
69+
The :pep:`508` requirement syntax can be used to explicitly state the project
70+
name (see :doc:`../topics/vcs-support`).
7171

7272
Satisfying Requirements
7373
-----------------------
@@ -367,21 +367,21 @@ Examples
367367

368368
.. code-block:: shell
369369
370-
python -m pip install -e 'git+https://git.repo/some_pkg.git#egg=SomePackage' # from git
371-
python -m pip install -e 'hg+https://hg.repo/some_pkg.git#egg=SomePackage' # from mercurial
372-
python -m pip install -e 'svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage' # from svn
373-
python -m pip install -e 'git+https://git.repo/some_pkg.git@feature#egg=SomePackage' # from 'feature' branch
374-
python -m pip install -e 'git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path' # install a python package from a repo subdirectory
370+
python -m pip install -e 'SomePackage @ git+https://git.repo/some_pkg.git' # from git
371+
python -m pip install -e 'SomePackage @ hg+https://hg.repo/some_pkg.git' # from mercurial
372+
python -m pip install -e 'SomePakcage @ svn+svn://svn.repo/some_pkg/trunk/' # from svn
373+
python -m pip install -e 'SomePackage @ git+https://git.repo/some_pkg.git@feature' # from 'feature' branch
374+
python -m pip install -e 'subdir @ git+https://git.repo/some_repo.git#subdirectory=subdir_path' # install a python package from a repo subdirectory
375375
376376
.. tab:: Windows
377377

378378
.. code-block:: shell
379379
380-
py -m pip install -e "git+https://git.repo/some_pkg.git#egg=SomePackage" # from git
381-
py -m pip install -e "hg+https://hg.repo/some_pkg.git#egg=SomePackage" # from mercurial
382-
py -m pip install -e "svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage" # from svn
383-
py -m pip install -e "git+https://git.repo/some_pkg.git@feature#egg=SomePackage" # from 'feature' branch
384-
py -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory
380+
py -m pip install -e "SomePackage @ git+https://git.repo/some_pkg.git" # from git
381+
py -m pip install -e "SomePackage @ hg+https://hg.repo/some_pkg.git" # from mercurial
382+
py -m pip install -e "SomePackage @ svn+svn://svn.repo/some_pkg/trunk/" # from svn
383+
py -m pip install -e "SomePackage @ git+https://git.repo/some_pkg.git@feature" # from 'feature' branch
384+
py -m pip install -e "subdir @ git+https://git.repo/some_repo.git#subdirectory=subdir_path" # install a python package from a repo subdirectory
385385
386386
#. Install a package with extras, i.e., optional dependencies
387387
(:ref:`specification <pypug:dependency-specifiers>`).

docs/html/topics/vcs-support.md

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,25 @@ control system being used). It is used through URL prefixes:
1010
- Subversion -- `svn+`
1111
- Bazaar -- `bzr+`
1212

13+
The general form of a VCS requirement is `ProjectName @ VCS_URL`, e.g.
14+
15+
```none
16+
MyProject @ git+https://git.example.com/MyProject
17+
MyProject[extra] @ git+https:/git.example.com/MyProject
18+
```
19+
20+
This is the Direct URL ({pep}`508`) requirement syntax. It is also permissible
21+
to remove `MyProject @` portion is removed and provide a bare VCS URL.
22+
23+
```none
24+
git+https://git.example.com/MyProject
25+
```
26+
27+
This is a pip specific extension. This form can be used as long as pip does
28+
not need to know the project name in advance. pip is generally able to infer
29+
the project name except in the case of {ref}`editable-vcs-installs`. In
30+
addition, extras cannot be requested using a bare VCS URL.
31+
1332
## Supported VCS
1433

1534
### Git
@@ -81,8 +100,8 @@ MyProject @ svn+ssh://[email protected]/MyProject
81100
You can also give specific revisions to an SVN URL, like so:
82101

83102
```none
84-
-e svn+http://svn.example.com/svn/MyProject/trunk@2019#egg=MyProject
85-
-e svn+http://svn.example.com/svn/MyProject/trunk@{20080101}#egg=MyProject
103+
-e MyProject @ svn+http://svn.example.com/svn/MyProject/trunk@2019
104+
-e MyProject @ svn+http://svn.example.com/svn/MyProject/trunk@{20080101}
86105
```
87106

88107
Note that you need to use [Editable VCS installs](#editable-vcs-installs) for
@@ -115,6 +134,9 @@ MyProject @ bzr+http://bzr.example.com/MyProject/[email protected]
115134
VCS projects can be installed in {ref}`editable mode <editable-installs>` (using
116135
the {ref}`--editable <install_--editable>` option) or not.
117136

137+
In editable mode, the project name must be provided upfront using the Direct URL
138+
(`MyProject @ URL`) form so pip can determine the VCS clone location.
139+
118140
- The default clone location (for editable installs) is:
119141

120142
- `<venv path>/src/SomeProject` in virtual environments
@@ -133,15 +155,15 @@ take on the VCS requirement (not the commit itself).
133155
## URL fragments
134156

135157
pip looks at the `subdirectory` fragments of VCS URLs for specifying the path to the
136-
Python package, when it is not in the root of the VCS directory. eg: `pkg_dir`.
158+
Python package, when it is not in the root of the VCS directory.
137159

138-
pip also looks at the `egg` fragment specifying the "project name". In practice the
139-
`egg` fragment is only required to help pip determine the VCS clone location in editable
140-
mode. In all other circumstances, the `egg` fragment is not necessary and its use is
141-
discouraged.
160+
```{note}
161+
pip also supports an `egg` fragment to specify the "project name". This is a legacy
162+
feature and its use is discouraged in favour of the Direct URL ({pep}`508`) form.
142163
143164
The `egg` fragment **should** be a bare {ref}`project name <pypug:name-normalization>`.
144165
Anything else is not guaranteed to work.
166+
```
145167

146168
````{admonition} Example
147169
If your repository layout is:
@@ -164,6 +186,6 @@ $ pip install "pkg @ vcs+protocol://repo_url/#subdirectory=pkg_dir"
164186
or:
165187
166188
```{pip-cli}
167-
$ pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir"
189+
$ pip install -e "pkg @ vcs+protocol://repo_url/#subdirectory=pkg_dir"
168190
```
169191
````

docs/html/user_guide.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ In practice, there are 4 common uses of Requirements files:
183183
``sometag``. You'd reference it in your requirements file with a line like
184184
so::
185185

186-
git+https://myvcs.com/some_dependency@sometag#egg=SomeDependency
186+
SomeDependency @ git+https://myvcs.com/some_dependency@sometag
187187

188188
If ``SomeDependency`` was previously a top-level requirement in your
189189
requirements file, then **replace** that line with the new line. If

news/13495.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support installing an editable requirement written as a Direct URL.

src/pip/_internal/req/constructors.py

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,25 @@ def _set_requirement_extras(req: Requirement, new_extras: set[str]) -> Requireme
8686
return get_requirement(f"{pre}{extras}{post}")
8787

8888

89-
def parse_editable(editable_req: str) -> tuple[str | None, str, set[str]]:
90-
"""Parses an editable requirement into:
91-
- a requirement name
92-
- an URL
93-
- extras
94-
- editable options
95-
Accepted requirements:
96-
svn+http://blahblah@rev#egg=Foobar[baz]&subdirectory=version_subdir
97-
.[some_extra]
98-
"""
89+
def _parse_direct_url_editable(editable_req: str) -> tuple[str | None, str, set[str]]:
90+
try:
91+
req = Requirement(editable_req)
92+
except InvalidRequirement:
93+
pass
94+
else:
95+
if req.url and "://" in req.url:
96+
# Join the marker back into the name part. This will be parsed out
97+
# later into a Requirement again.
98+
if req.marker:
99+
name = f"{req.name} ; {req.marker}"
100+
else:
101+
name = req.name
102+
return (name, req.url, req.extras)
103+
104+
raise ValueError
99105

106+
107+
def _parse_pip_syntax_editable(editable_req: str) -> tuple[str | None, str, set[str]]:
100108
url = editable_req
101109

102110
# If a file path is specified with extras, strip off the extras.
@@ -122,23 +130,41 @@ def parse_editable(editable_req: str) -> tuple[str | None, str, set[str]]:
122130
url = f"{version_control}+{url}"
123131
break
124132

133+
return Link(url).egg_fragment, url, set()
134+
135+
136+
def parse_editable(editable_req: str) -> tuple[str | None, str, set[str]]:
137+
"""Parses an editable requirement into:
138+
- a requirement name
139+
- an URL
140+
- extras
141+
Accepted requirements:
142+
- svn+http://blahblah@rev#egg=Foobar[baz]&subdirectory=version_subdir
143+
- local_path[some_extra]
144+
- Foobar[extra] @ svn+http://blahblah@rev#subdirectory=subdir ; markers
145+
"""
146+
try:
147+
package_name, url, extras = _parse_direct_url_editable(editable_req)
148+
except ValueError:
149+
package_name, url, extras = _parse_pip_syntax_editable(editable_req)
150+
125151
link = Link(url)
126152

127-
if not link.is_vcs:
153+
if not link.is_vcs and not link.is_file:
128154
backends = ", ".join(vcs.all_schemes)
129155
raise InstallationError(
130156
f"{editable_req} is not a valid editable requirement. "
131157
f"It should either be a path to a local project or a VCS URL "
132158
f"(beginning with {backends})."
133159
)
134160

135-
package_name = link.egg_fragment
136-
if not package_name:
161+
# The project name can be inferred from local file URIs easily.
162+
if not package_name and not link.is_file:
137163
raise InstallationError(
138164
f"Could not detect requirement name for '{editable_req}', "
139-
"please specify one with #egg=your_package_name"
165+
"please specify one with your_package_name @ URL"
140166
)
141-
return package_name, url, set()
167+
return package_name, url, extras
142168

143169

144170
def check_first_requirement_in_file(filename: str) -> None:

src/pip/_internal/resolution/resolvelib/candidates.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,12 @@ def make_install_req_from_editable(
8585
link: Link, template: InstallRequirement
8686
) -> InstallRequirement:
8787
assert template.editable, "template not editable"
88+
if template.name:
89+
req_string = f"{template.name} @ {link.url}"
90+
else:
91+
req_string = link.url
8892
ireq = install_req_from_editable(
89-
link.url,
93+
req_string,
9094
user_supplied=template.user_supplied,
9195
comes_from=template.comes_from,
9296
use_pep517=template.use_pep517,

tests/functional/test_install_reqs.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
PipTestEnvironment,
1111
ResolverVariant,
1212
TestData,
13+
_create_test_package,
1314
_create_test_package_with_subdirectory,
1415
create_basic_sdist_for_package,
1516
create_basic_wheel_for_package,
@@ -941,3 +942,52 @@ def test_nonpep517_setuptools_import_failure(script: PipTestEnvironment) -> None
941942
exc_message = "ImportError: this 'setuptools' was intentionally poisoned"
942943
assert nice_message in result.stderr
943944
assert exc_message in result.stderr
945+
946+
947+
class TestEditableDirectURL:
948+
def test_install_local_project(
949+
self, script: PipTestEnvironment, data: TestData, common_wheels: Path
950+
) -> None:
951+
uri = (data.src / "simplewheel-2.0").as_uri()
952+
script.pip(
953+
"install", "--no-index", "-e", f"simplewheel @ {uri}", "-f", common_wheels
954+
)
955+
script.assert_installed(simplewheel="2.0")
956+
957+
def test_install_local_project_with_extra(
958+
self, script: PipTestEnvironment, data: TestData, common_wheels: Path
959+
) -> None:
960+
uri = (data.src / "requires_simple_extra").as_uri()
961+
script.pip(
962+
"install",
963+
"--no-index",
964+
"-e",
965+
f"requires-simple-extra[extra] @ {uri}",
966+
"-f",
967+
common_wheels,
968+
"-f",
969+
data.packages,
970+
)
971+
script.assert_installed(requires_simple_extra="0.1")
972+
script.assert_installed(simple="1.0")
973+
974+
def test_install_local_git_repo(
975+
self, script: PipTestEnvironment, common_wheels: Path
976+
) -> None:
977+
repo_path = _create_test_package(script.scratch_path, "simple")
978+
url = "git+" + repo_path.as_uri()
979+
script.pip(
980+
"install", "--no-index", "-e", f"simple @ {url}", "-f", common_wheels
981+
)
982+
script.assert_installed(simple="0.1")
983+
984+
@pytest.mark.network
985+
def test_install_remote_git_repo_with_extra(
986+
self, script: PipTestEnvironment, data: TestData, common_wheels: Path
987+
) -> None:
988+
req = "pip-test-package[extra] @ git+https://github.com/pypa/pip-test-package"
989+
script.pip(
990+
"install", "--no-index", "-e", req, "-f", common_wheels, "-f", data.packages
991+
)
992+
script.assert_installed(pip_test_package="0.1.1")
993+
script.assert_installed(simple="3.0")

tests/unit/test_req.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,25 @@ def test_install_req_extend_extras(
853853
assert extended.permit_editable_wheels == req.permit_editable_wheels
854854

855855

856+
@pytest.mark.parametrize(
857+
"req_str, expected",
858+
[
859+
(
860+
'foo[extra] @ svn+http://foo ; os_name == "nt"',
861+
('foo ; os_name == "nt"', "svn+http://foo", {"extra"}),
862+
),
863+
(
864+
"foo @ svn+http://foo",
865+
("foo", "svn+http://foo", set()),
866+
),
867+
],
868+
)
869+
def test_parse_editable_pep508(
870+
req_str: str, expected: tuple[str, str, set[str]]
871+
) -> None:
872+
assert parse_editable(req_str) == expected
873+
874+
856875
@mock.patch("pip._internal.req.req_install.os.path.abspath")
857876
@mock.patch("pip._internal.req.req_install.os.path.exists")
858877
@mock.patch("pip._internal.req.req_install.os.path.isdir")

0 commit comments

Comments
 (0)