Skip to content

Commit de12f14

Browse files
committed
Fix pex3 wheel and add a test.
1 parent 0bcefd9 commit de12f14

File tree

6 files changed

+169
-104
lines changed

6 files changed

+169
-104
lines changed

pex/cli/commands/pip/core.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from __future__ import absolute_import
55

6+
import os.path
67
from argparse import Namespace, _ActionsContainer
78

89
from pex import dependency_configuration
@@ -94,8 +95,14 @@ class SourceDist(object):
9495
subdirectory = attr.ib(default=None) # type: Optional[str]
9596

9697

98+
@attr.s(frozen=True)
99+
class LocalProject(object):
100+
path = attr.ib() # type: str
101+
editable = attr.ib() # type: bool
102+
103+
97104
if TYPE_CHECKING:
98-
Dist = Union[SourceDist, WheelDist]
105+
Dist = Union[LocalProject, SourceDist, WheelDist]
99106
DownloadedItem = Union[DownloadedArtifact, LocalDistribution]
100107

101108

@@ -106,13 +113,15 @@ def to_dist(downloaded_artifact):
106113
# type: (DownloadedItem) -> Dist
107114
if is_wheel(downloaded_artifact.path):
108115
return WheelDist(downloaded_artifact.path)
116+
if os.path.isdir(downloaded_artifact.path):
117+
return LocalProject(downloaded_artifact.path, editable=downloaded_artifact.editable)
109118
return SourceDist(downloaded_artifact.path, subdirectory=downloaded_artifact.subdirectory)
110119

111120
return tuple(map(to_dist, downloaded_artifacts))
112121

113122

114123
def download_distributions(configuration):
115-
# type: (Configuration) -> Union[Tuple[Union[SourceDist, WheelDist], ...], Error]
124+
# type: (Configuration) -> Union[Tuple[Union[LocalProject, SourceDist, WheelDist], ...], Error]
116125

117126
requirement_configuration = configuration.requirement_configuration
118127
dep_configuration = configuration.dependency_configuration

pex/cli/commands/pip/download.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from pex.cli.command import BuildTimeCommand
1010
from pex.cli.commands.pip import core
11+
from pex.cli.commands.pip.core import LocalProject
1112
from pex.common import safe_copy, safe_mkdir
1213
from pex.result import Ok, Result, try_
1314

@@ -35,6 +36,8 @@ def run(self):
3536

3637
safe_mkdir(self.options.dest_dir)
3738
for dist in dists:
39+
if isinstance(dist, LocalProject):
40+
continue
3841
safe_copy(dist.path, os.path.join(self.options.dest_dir, os.path.basename(dist.path)))
3942

4043
return Ok()

pex/cli/commands/pip/wheel.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from pex.cli.command import BuildTimeCommand
1111
from pex.cli.commands.pip import core
12-
from pex.cli.commands.pip.core import SourceDist, WheelDist
12+
from pex.cli.commands.pip.core import LocalProject, SourceDist, WheelDist
1313
from pex.common import safe_copy, safe_mkdir
1414
from pex.resolver import BuildRequest
1515
from pex.result import Ok, Result, try_
@@ -41,30 +41,38 @@ def run(self):
4141
dists = try_(core.download_distributions(configuration))
4242

4343
wheels = OrderedDict() # type: OrderedDict[str, str]
44+
local_projects = [] # type: List[LocalProject]
4445
sdists = [] # type: List[SourceDist]
4546
for dist in dists:
4647
if isinstance(dist, WheelDist):
4748
wheels[os.path.basename(dist.path)] = dist.path
49+
elif isinstance(dist, LocalProject):
50+
local_projects.append(dist)
4851
else:
4952
sdists.append(dist)
5053

51-
if sdists:
54+
if local_projects or sdists:
55+
build_requests = [] # type: List[BuildRequest]
56+
for target in configuration.resolve_targets().unique_targets():
57+
for local_project in local_projects:
58+
build_requests.append(
59+
BuildRequest.for_directory(
60+
target=target,
61+
source_path=local_project.path,
62+
editable=local_project.editable,
63+
)
64+
)
65+
for sdist in sdists:
66+
build_requests.append(
67+
BuildRequest.for_file(
68+
target=target,
69+
source_path=sdist.path,
70+
subdirectory=sdist.subdirectory,
71+
)
72+
)
5273
wheels.update(
5374
(os.path.basename(wheel), wheel)
54-
for wheel in try_(
55-
core.build_wheels(
56-
configuration,
57-
tuple(
58-
BuildRequest.for_file(
59-
target=target,
60-
source_path=sdist.path,
61-
subdirectory=sdist.subdirectory,
62-
)
63-
for sdist in sdists
64-
for target in configuration.resolve_targets().unique_targets()
65-
),
66-
)
67-
)
75+
for wheel in try_(core.build_wheels(configuration, build_requests))
6876
)
6977

7078
safe_mkdir(self.options.dest_dir)

testing/local_project.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Copyright 2026 Pex project contributors.
2+
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3+
4+
from __future__ import absolute_import, print_function
5+
6+
import os
7+
from textwrap import dedent
8+
9+
from pex.common import safe_open
10+
11+
12+
class LocalProject(str):
13+
def edit_all_caps(self, all_caps):
14+
# type: (bool) -> None
15+
with open(os.path.join(self, "local_project.py"), "a") as fp:
16+
print("ALL_CAPS={all_caps!r}".format(all_caps=all_caps), file=fp)
17+
18+
19+
def create(project_dir):
20+
# type: (str) -> LocalProject
21+
with safe_open(os.path.join(project_dir, "local_project.py"), "w") as fp:
22+
fp.write(
23+
dedent(
24+
"""\
25+
from __future__ import print_function
26+
27+
import sys
28+
29+
30+
ALL_CAPS = False
31+
32+
33+
def main():
34+
text = sys.argv[1:]
35+
if ALL_CAPS:
36+
text[:] = [item.upper() for item in text]
37+
print(*text, end="")
38+
39+
"""
40+
)
41+
)
42+
with safe_open(os.path.join(project_dir, "setup.cfg"), "w") as fp:
43+
fp.write(
44+
dedent(
45+
"""\
46+
[metadata]
47+
name = local_project
48+
version = 0.0.1
49+
50+
[options]
51+
py_modules =
52+
local_project
53+
54+
[options.entry_points]
55+
console_scripts =
56+
local-project = local_project:main
57+
"""
58+
)
59+
)
60+
with safe_open(os.path.join(project_dir, "setup.py"), "w") as fp:
61+
fp.write("from setuptools import setup; setup()")
62+
with open(os.path.join(project_dir, "pyproject.toml"), "w") as fp:
63+
fp.write(
64+
dedent(
65+
"""\
66+
[build-system]
67+
requires = ["setuptools"]
68+
build-backend = "setuptools.build_meta"
69+
"""
70+
)
71+
)
72+
return LocalProject(project_dir)

tests/integration/cli/commands/test_wheel.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@
1111

1212
import pytest
1313

14+
from pex.dist_metadata import ProjectNameAndVersion
1415
from pex.interpreter import PythonInterpreter
1516
from pex.interpreter_constraints import COMPATIBLE_PYTHON_VERSIONS
1617
from pex.os import LINUX, MAC, WINDOWS
18+
from pex.pep_503 import ProjectName
19+
from pex.venv.virtualenv import InstallationChoice, Virtualenv
1720
from testing.cli import run_pex3
21+
from testing.local_project import create as create_local_project
1822
from testing.pytest_utils.tmp import Tempdir
1923

2024

@@ -221,3 +225,37 @@ def test_vcs_subdir_via_pex_lock(tmpdir):
221225
"wheel", "--pip-version", "latest-compatible", "--lock", lock, "-d", dest_dir
222226
).assert_success()
223227
assert [EXPECTED_SDEV_LOGGING_UTILS_WHL] == os.listdir(dest_dir)
228+
229+
230+
@pytest.mark.skipif(
231+
sys.version_info < (3, 7),
232+
reason=(
233+
"Modern setuptools (>= 64.0.0) with support for pep-660 build_editable is required, and "
234+
"setuptools 64 requires at least Python 3.7."
235+
),
236+
)
237+
def test_wheel_editable(tmpdir):
238+
# type: (Tempdir) -> None
239+
240+
local_project = create_local_project(tmpdir.join("project"))
241+
dest_dir = tmpdir.join("dest")
242+
run_pex3("wheel", "-e", local_project, "ansicolors==1.1.8", "-d", dest_dir).assert_success()
243+
244+
wheels_by_project_name = {
245+
ProjectNameAndVersion.from_filename(whl).canonicalized_project_name: whl
246+
for whl in os.listdir(dest_dir)
247+
}
248+
assert "ansicolors-1.1.8-py2.py3-none-any.whl" == wheels_by_project_name.pop(
249+
ProjectName("ansicolors")
250+
)
251+
local_project_editable_whl = wheels_by_project_name.pop(ProjectName("local_project"))
252+
assert not wheels_by_project_name
253+
254+
venv = Virtualenv.create(
255+
tmpdir.join("venv"),
256+
install_pip=InstallationChoice.YES,
257+
other_installs=[os.path.join(dest_dir, local_project_editable_whl)],
258+
)
259+
assert b"foo" == subprocess.check_output(args=[venv.bin_path("local-project"), "foo"])
260+
local_project.edit_all_caps(True)
261+
assert b"FOO" == subprocess.check_output(args=[venv.bin_path("local-project"), "foo"])

0 commit comments

Comments
 (0)