Skip to content

Commit e13f168

Browse files
authored
Fix editable requirement parsing. (#2464)
Previously, editable requirements in requirements files were not parsed properly by Pex. Although they did not trigger parse errors, PEXes created from editable requirements would fail to import those requirements at runtime despite the editable project distribution being embedded in the PEX file. Fixes #2410
1 parent 9471d7f commit e13f168

6 files changed

+114
-9
lines changed

CHANGES.md

+9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Release Notes
22

3+
## 2.10.1
4+
5+
This release fixes a long-standing bug in Pex parsing of editable
6+
requirements. This bug caused PEXes containing local editable project
7+
requirements to fail to import those local editable projects despite
8+
the fact the PEX itself contained them.
9+
10+
* Fix editable requirement parsing. (#2464)
11+
312
## 2.10.0
413

514
This release adds support for injecting requirements into the isolated

docs-requirements.txt

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@ furo
22
httpx
33
myst-parser[linkify]
44
sphinx
5-
sphinx-simplepdf
5+
sphinx-simplepdf
6+
7+
# The 0.11.0 release removes deprecated API parameters which breaks weasyprint (62.3 depends on
8+
# `pydyf>=0.10.0`) which is a dependency of sphinx-simplepdf.
9+
pydyf<0.11.0

pex/requirements.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,9 @@ def _try_parse_pip_local_formats(
418418
directory_name, requirement_parts = match.groups()
419419
stripped_path = os.path.join(os.path.dirname(path), directory_name)
420420
abs_stripped_path = (
421-
os.path.join(basepath, stripped_path) if basepath else os.path.abspath(stripped_path)
421+
os.path.join(basepath, stripped_path)
422+
if basepath and not os.path.isabs(stripped_path)
423+
else os.path.abspath(stripped_path)
422424
)
423425
if not os.path.exists(abs_stripped_path):
424426
return None
@@ -650,8 +652,11 @@ def parse_requirements(
650652
yield requirement
651653
continue
652654

653-
# Skip empty lines, comment lines and all other Pip options.
654-
if not processed_text or processed_text.startswith("-"):
655+
# Skip empty lines, comment lines and all Pip global options.
656+
if not processed_text or (
657+
processed_text.startswith("-")
658+
and not re.match(r"^(?:-e|--editable)\s.*", processed_text)
659+
):
655660
continue
656661

657662
# Only requirement lines remain.

pex/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# Copyright 2015 Pex project contributors.
22
# Licensed under the Apache License, Version 2.0 (see LICENSE).
33

4-
__version__ = "2.10.0"
4+
__version__ = "2.10.1"

tests/integration/test_issue_2410.py

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Copyright 2024 Pex project contributors.
2+
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3+
4+
from __future__ import absolute_import
5+
6+
import os.path
7+
import subprocess
8+
from textwrap import dedent
9+
10+
from colors import colors
11+
12+
from pex.common import safe_open
13+
from pex.typing import TYPE_CHECKING
14+
from testing import run_pex_command
15+
16+
if TYPE_CHECKING:
17+
from typing import Any
18+
19+
20+
def test_pex_with_editable(tmpdir):
21+
# type: (Any) -> None
22+
23+
project_dir = os.path.join(str(tmpdir), "project")
24+
with safe_open(os.path.join(project_dir, "example.py"), "w") as fp:
25+
fp.write(
26+
dedent(
27+
"""\
28+
import sys
29+
30+
import colors
31+
32+
33+
def colorize(*messages):
34+
return colors.green(" ".join(messages))
35+
36+
37+
if __name__ == "__main__":
38+
print(colorize(*sys.argv[1:]))
39+
sys.exit(0)
40+
"""
41+
)
42+
)
43+
with safe_open(os.path.join(project_dir, "setup.py"), "w") as fp:
44+
fp.write(
45+
dedent(
46+
"""\
47+
from setuptools import setup
48+
49+
50+
setup(
51+
name="example",
52+
version="0.1.0",
53+
py_modules=["example"],
54+
)
55+
"""
56+
)
57+
)
58+
59+
requirements = os.path.join(project_dir, "requirements.txt")
60+
with safe_open(requirements, "w") as fp:
61+
fp.write(
62+
dedent(
63+
"""\
64+
ansicolors==1.1.8
65+
-e file://{project_dir}
66+
"""
67+
).format(project_dir=project_dir)
68+
)
69+
70+
pex = os.path.join(str(tmpdir), "pex")
71+
run_pex_command(args=["-r", requirements, "-m", "example", "-o", pex]).assert_success()
72+
output = (
73+
subprocess.check_output(args=[pex, "A", "wet", "duck", "flies", "at", "night!"])
74+
.decode("utf-8")
75+
.strip()
76+
)
77+
assert colors.green("A wet duck flies at night!") == output, output

tests/test_requirements.py

+14-4
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def chroot():
5151
curdir = os.getcwd()
5252
try:
5353
os.chdir(chroot)
54-
yield chroot
54+
yield os.path.realpath(chroot)
5555
finally:
5656
os.chdir(curdir)
5757

@@ -278,8 +278,8 @@ def test_parse_requirements_stress(chroot):
278278
hg+http://hg.example.com/MyProject@da39a3ee5e6b\\
279279
#egg=AnotherProject[extra,more] ; python_version=="3.9.*"&subdirectory=foo/bar
280280
281-
ftp://a/${PROJECT_NAME}-1.0.tar.gz
282-
http://a/${PROJECT_NAME}-1.0.zip
281+
ftp://a/${{PROJECT_NAME}}-1.0.tar.gz
282+
http://a/${{PROJECT_NAME}}-1.0.zip
283283
https://a/numpy-1.9.2-cp34-none-win32.whl
284284
https://a/numpy-1.9.2-cp34-none-win32.whl;\\
285285
python_version=="3.4.*" and sys_platform=='win32'
@@ -295,8 +295,14 @@ def test_parse_requirements_stress(chroot):
295295
296296
# Wheel with local version
297297
http://download.pytorch.org/whl/cpu/torch-1.12.1%2Bcpu-cp310-cp310-linux_x86_64.whl
298+
299+
# Editable
300+
-e file://{chroot}/extra/a/local/project
301+
--editable file://{chroot}/extra/a/local/project/
302+
-e ./another/local/project
303+
--editable ./another/local/project/
298304
"""
299-
)
305+
).format(chroot=chroot)
300306
)
301307
touch("extra/pyproject.toml")
302308
touch("extra/a/local/project/pyproject.toml")
@@ -470,6 +476,10 @@ def test_parse_requirements_stress(chroot):
470476
url="http://download.pytorch.org/whl/cpu/torch-1.12.1%2Bcpu-cp310-cp310-linux_x86_64.whl",
471477
specifier="==1.12.1+cpu",
472478
),
479+
local_req(path=os.path.join(chroot, "extra/a/local/project"), editable=True),
480+
local_req(path=os.path.join(chroot, "extra/a/local/project"), editable=True),
481+
local_req(path=os.path.join(chroot, "extra/another/local/project"), editable=True),
482+
local_req(path=os.path.join(chroot, "extra/another/local/project"), editable=True),
473483
url_req(
474484
project_name="numpy",
475485
url=os.path.realpath("./downloads/numpy-1.9.2-cp34-none-win32.whl"),

0 commit comments

Comments
 (0)