Skip to content

Commit 196f271

Browse files
bhimrazypre-commit-ci[bot]Copilot
authored
Remove deprecated pkg_resources usage for setuptools >= 82 compatibility (#473)
* drop pkg resources dependency * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add unit tests for requirements parsing and adjustment functions * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * update * Apply suggestions Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Apply suggestions Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor requirement parsing to ensure string conversion of requirements * Enhance upper bound removal logic in requirement adjustment and add corresponding tests * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix line continuation handling in requirement parsing and add corresponding tests * Refactor yield_lines to _yield_lines for consistency and update tests accordingly * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 0701f88 commit 196f271

File tree

7 files changed

+190
-20
lines changed

7 files changed

+190
-20
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
### Fixed
2222

2323
- Fixed `compare_version` if runtime error ([#427](https://github.com/Lightning-AI/utilities/pull/427))
24+
- Remove deprecated `pkg_resources` usage for `setuptools >= 82` compatibility ([#473](https://github.com/Lightning-AI/utilities/pull/473))
2425

2526

2627
---

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[build-system]
22
requires = [
3+
"packaging",
34
"setuptools",
45
"wheel",
56
]

setup.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
#!/usr/bin/env python
22
import glob
33
import os
4+
from collections.abc import Iterator
45
from importlib.util import module_from_spec, spec_from_file_location
56

6-
from pkg_resources import parse_requirements
77
from setuptools import find_packages, setup
88

99
_PATH_ROOT = os.path.realpath(os.path.dirname(__file__))
@@ -19,10 +19,17 @@ def _load_py_module(fname: str, pkg: str = "lightning_utilities"):
1919

2020

2121
about = _load_py_module("__about__.py")
22+
requirements_module = _load_py_module(os.path.join("install", "requirements.py"))
23+
24+
25+
# load basic requirements using the central parser from lightning_utilities.install.requirements
26+
def _parse_requirements(lines: list[str]) -> Iterator[str]:
27+
"""Parse requirements from lines using the canonical parser."""
28+
return (str(req) for req in requirements_module._parse_requirements(lines))
29+
2230

23-
# load basic requirements
2431
with open(os.path.join(_PATH_REQUIRE, "core.txt")) as fp:
25-
requirements = list(map(str, parse_requirements(fp.readlines())))
32+
requirements = list(_parse_requirements(fp.readlines()))
2633

2734

2835
# make extras as automated loading
@@ -36,8 +43,7 @@ def _requirement_extras(path_req: str = _PATH_REQUIRE) -> dict:
3643
continue
3744
name, _ = os.path.splitext(fname)
3845
with open(fpath) as fp:
39-
reqs = parse_requirements(fp.readlines())
40-
extras[name] = list(map(str, reqs))
46+
extras[name] = list(_parse_requirements(fp.readlines()))
4147
return extras
4248

4349

src/lightning_utilities/docs/formatting.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,15 @@ def _load_pypi_versions(package_name: str) -> list[str]:
8686
['0.9', '0.10', '0.11', '0.12', ...]
8787
8888
"""
89-
from distutils.version import LooseVersion
90-
9189
import requests
90+
from packaging.version import Version
9291

9392
url = f"https://pypi.org/pypi/{package_name}/json"
9493
data = requests.get(url, timeout=10).json()
9594
versions = data["releases"].keys()
9695
# filter all version which include only numbers and dots
9796
versions = {k for k in versions if re.match(r"^\d+(\.\d+)*$", k)}
98-
return sorted(versions, key=LooseVersion)
97+
return sorted(versions, key=Version)
9998

10099

101100
def _update_link_based_imported_package(link: str, pkg_ver: str, version_digits: Optional[int]) -> str:

src/lightning_utilities/install/requirements.py

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,25 @@
1010

1111
import re
1212
from collections.abc import Iterable, Iterator
13-
from distutils.version import LooseVersion
1413
from pathlib import Path
1514
from typing import Any, Optional, Union
1615

17-
from pkg_resources import Requirement, yield_lines # type: ignore[import-untyped]
16+
from packaging.requirements import Requirement
17+
from packaging.version import Version
18+
19+
20+
def _yield_lines(strs: Union[str, Iterable[str]]) -> Iterator[str]:
21+
"""Yield non-empty, non-comment lines from a string or iterable of strings.
22+
23+
Adapted from pkg_resources.yield_lines.
24+
25+
"""
26+
if isinstance(strs, str):
27+
strs = strs.splitlines()
28+
for line in strs:
29+
line = line.strip()
30+
if line and not line.startswith("#"):
31+
yield line
1832

1933

2034
class _RequirementWithComment(Requirement):
@@ -77,16 +91,20 @@ def adjust(self, unfreeze: str) -> str:
7791
if self.strict:
7892
return f"{out} {self.strict_string}"
7993
if unfreeze == "major":
80-
for operator, version in self.specs:
81-
if operator in ("<", "<="):
82-
major = LooseVersion(version).version[0]
94+
for spec in self.specifier:
95+
if spec.operator in ("<", "<="):
96+
major = Version(spec.version).major
8397
# replace upper bound with major version increased by one
84-
return out.replace(f"{operator}{version}", f"<{int(major) + 1}.0")
98+
return out.replace(f"{spec.operator}{spec.version}", f"<{int(major) + 1}.0")
8599
elif unfreeze == "all":
86-
for operator, version in self.specs:
87-
if operator in ("<", "<="):
88-
# drop upper bound
89-
return out.replace(f"{operator}{version},", "")
100+
for spec in self.specifier:
101+
if spec.operator in ("<", "<="):
102+
# drop upper bound (with or without trailing/leading comma)
103+
upper = f"{spec.operator}{spec.version}"
104+
result = out.replace(f"{upper},", "").replace(f",{upper}", "")
105+
if upper in result:
106+
result = result.replace(upper, "")
107+
return result.strip()
90108
elif unfreeze != "none":
91109
raise ValueError(f"Unexpected unfreeze: {unfreeze!r} value.")
92110
return out
@@ -113,7 +131,7 @@ def _parse_requirements(strs: Union[str, Iterable[str]]) -> Iterator[_Requiremen
113131
_RequirementWithComment: Parsed requirement objects with preserved comment and pip argument.
114132
115133
"""
116-
lines = yield_lines(strs)
134+
lines = _yield_lines(strs)
117135
pip_argument = None
118136
for line in lines:
119137
# Drop comments -- a hash without a space may be in a URL.
@@ -124,7 +142,7 @@ def _parse_requirements(strs: Union[str, Iterable[str]]) -> Iterator[_Requiremen
124142
comment = ""
125143
# If there is a line continuation, drop it, and append the next line.
126144
if line.endswith("\\"):
127-
line = line[:-2].strip()
145+
line = line[:-1].strip()
128146
try:
129147
line += next(lines)
130148
except StopIteration:

tests/unittests/install/__init__.py

Whitespace-only changes.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
from lightning_utilities.install.requirements import (
6+
_parse_requirements,
7+
_RequirementWithComment,
8+
_yield_lines,
9+
load_requirements,
10+
)
11+
12+
_PATH_ROOT = Path(__file__).parent.parent.parent.parent
13+
14+
15+
def test_yield_lines_from_list():
16+
assert list(_yield_lines(["foo", " bar ", "", "# comment", "baz"])) == ["foo", "bar", "baz"]
17+
18+
19+
def test_yield_lines_from_string():
20+
assert list(_yield_lines("foo\n bar \n\n# comment\nbaz")) == ["foo", "bar", "baz"]
21+
22+
23+
def test_yield_lines_empty():
24+
assert list(_yield_lines([])) == []
25+
assert list(_yield_lines("")) == []
26+
27+
28+
def test_requirement_with_comment_attributes():
29+
req = _RequirementWithComment("arrow>=1.0", comment="# my comment")
30+
assert req.name == "arrow"
31+
assert req.comment == "# my comment"
32+
assert req.pip_argument is None
33+
assert req.strict is False
34+
35+
36+
def test_requirement_with_comment_strict():
37+
assert _RequirementWithComment("arrow>=1.0", comment="# strict").strict is True
38+
assert _RequirementWithComment("arrow>=1.0", comment="# Strict pinning").strict is True
39+
40+
41+
def test_requirement_with_comment_pip_argument():
42+
req = _RequirementWithComment("arrow>=1.0", pip_argument="--extra-index-url https://x")
43+
assert req.pip_argument == "--extra-index-url https://x"
44+
45+
with pytest.raises(RuntimeError, match="wrong pip argument"):
46+
_RequirementWithComment("arrow>=1.0", pip_argument="")
47+
48+
49+
def test_adjust_none():
50+
assert _RequirementWithComment("arrow<=1.2,>=1.0").adjust("none") == "arrow<=1.2,>=1.0"
51+
assert (
52+
_RequirementWithComment("arrow<=1.2,>=1.0", comment="# strict").adjust("none") == "arrow<=1.2,>=1.0 # strict"
53+
)
54+
55+
56+
def test_adjust_all():
57+
assert _RequirementWithComment("arrow<=1.2,>=1.0").adjust("all") == "arrow>=1.0"
58+
assert _RequirementWithComment("arrow>=1.0,<=1.2").adjust("all") == "arrow>=1.0"
59+
assert _RequirementWithComment("arrow<=1.2").adjust("all") == "arrow"
60+
assert _RequirementWithComment("arrow<=1.2,>=1.0", comment="# strict").adjust("all") == "arrow<=1.2,>=1.0 # strict"
61+
assert _RequirementWithComment("arrow").adjust("all") == "arrow"
62+
63+
64+
def test_adjust_major():
65+
assert _RequirementWithComment("arrow>=1.2.0, <=1.2.2").adjust("major") == "arrow<2.0,>=1.2.0"
66+
assert _RequirementWithComment("lib>=0.5, <=0.9").adjust("major") == "lib<1.0,>=0.5"
67+
assert (
68+
_RequirementWithComment("arrow>=1.2.0, <=1.2.2", comment="# strict").adjust("major")
69+
== "arrow<=1.2.2,>=1.2.0 # strict"
70+
)
71+
assert _RequirementWithComment("arrow>=1.2.0").adjust("major") == "arrow>=1.2.0"
72+
73+
74+
def test_adjust_invalid_unfreeze():
75+
with pytest.raises(ValueError, match="Unexpected unfreeze"):
76+
_RequirementWithComment("arrow>=1.0").adjust("invalid")
77+
78+
79+
def test_parse_requirements_basic():
80+
reqs = list(_parse_requirements(["# comment", "", "numpy>=1.0", "pandas<2.0"]))
81+
assert [str(r) for r in reqs] == ["numpy>=1.0", "pandas<2.0"]
82+
83+
84+
def test_parse_requirements_from_string():
85+
reqs = list(_parse_requirements("# comment\n\nnumpy>=1.0\npandas<2.0"))
86+
assert [str(r) for r in reqs] == ["numpy>=1.0", "pandas<2.0"]
87+
88+
89+
def test_parse_requirements_preserves_comments():
90+
reqs = list(_parse_requirements(["arrow>=1.0 # strict"]))
91+
assert len(reqs) == 1
92+
assert reqs[0].comment == " # strict"
93+
assert reqs[0].strict is True
94+
95+
96+
def test_parse_requirements_pip_argument():
97+
reqs = list(_parse_requirements(["--extra-index-url https://x", "torch>=2.0"]))
98+
assert len(reqs) == 1
99+
assert reqs[0].pip_argument == "--extra-index-url https://x"
100+
101+
102+
def test_parse_requirements_skips():
103+
reqs = list(_parse_requirements(["-r other.txt", "pesq @ git+https://github.com/foo/bar", "numpy"]))
104+
assert len(reqs) == 1
105+
assert reqs[0].name == "numpy"
106+
107+
108+
def test_parse_requirements_line_continuation():
109+
reqs = list(_parse_requirements(["foo\\", ">=1.0"]))
110+
assert len(reqs) == 1
111+
assert str(reqs[0]) == "foo>=1.0"
112+
113+
reqs = list(_parse_requirements(["bar \\", ">=2.0,<3.0"]))
114+
assert len(reqs) == 1
115+
assert str(reqs[0]) == "bar<3.0,>=2.0"
116+
117+
118+
def test_load_requirements_core():
119+
path_req = str(_PATH_ROOT / "requirements")
120+
reqs = load_requirements(path_req, "core.txt", unfreeze="all")
121+
assert len(reqs) > 0
122+
# Verify that load_requirements returns a cleaned list of requirement strings
123+
assert all(isinstance(r, str) for r in reqs)
124+
assert all(r for r in reqs) # no empty strings
125+
assert all(not r.lstrip().startswith("#") for r in reqs) # no comment lines
126+
assert all(r == r.strip() for r in reqs) # no leading/trailing whitespace
127+
128+
129+
def test_load_requirements_nonexistent(tmpdir):
130+
with pytest.raises(FileNotFoundError):
131+
load_requirements(str(tmpdir), "nonexistent.txt")
132+
133+
134+
def test_load_requirements_invalid_unfreeze(tmpdir):
135+
with pytest.raises(ValueError, match="unsupported"):
136+
load_requirements(str(tmpdir), "x.txt", unfreeze="bad")
137+
138+
139+
def test_load_requirements_unfreeze_strategies(tmpdir):
140+
req_file = tmpdir / "test.txt"
141+
req_file.write("arrow>=1.2.0, <=1.2.2\n")
142+
143+
assert load_requirements(str(tmpdir), "test.txt", unfreeze="none") == ["arrow<=1.2.2,>=1.2.0"]
144+
assert load_requirements(str(tmpdir), "test.txt", unfreeze="major") == ["arrow<2.0,>=1.2.0"]
145+
assert load_requirements(str(tmpdir), "test.txt", unfreeze="all") == ["arrow>=1.2.0"]

0 commit comments

Comments
 (0)