v4: Read node pack and pip versions that carry a suffix (#3516)
StrictVersion read each of the first three components of a version with int(), so it raised on a suffix after the third number. On v4 it reads node pack versions and the pip versions of pip_auto_fix.list. - Read a node pack version with a pre-release after the third number, following '.' or '-', and write it with '-' as before: a registry version such as 1.8.0-beta.1 is recognized, and 0.1.3.post1 no longer stops the scan of installed node packs, in the glob and the legacy core - Ask a packaging Specifier for the entries of pip_auto_fix.list, as pip does: an installed 2.5.1+cu124 or 4.41.0rc1 no longer skips the entry with "Failed to restore", and "~=" becomes a compatible-release clause - Add tests that run the real scan of both cores and the real PIPFixer - Add a CHANGELOG entry The same change for main is #3431, which also moves the pip checks at install time to packaging; v4 did that in #3489.
This commit is contained in:
@@ -105,6 +105,18 @@ and this project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/
|
||||
subgraph. It read the node's links from the root graph, so it stopped with an
|
||||
error and left the same two copies of the node. Links to the inputs and
|
||||
outputs of the subgraph are kept as well.
|
||||
- Read the version of a node pack installed from the registry when it has a
|
||||
pre-release, such as `1.8.0-beta.1` or `0.7.2-b2`; such a pack was not
|
||||
recognized as a registry pack. A version with a fourth part, such as
|
||||
`0.1.3.post1`, stopped the scan of installed node packs with
|
||||
`invalid literal for int()`, in the server and in `cm-cli` (#3431).
|
||||
- Compare the versions in `pip_auto_fix.list` as pip does. An entry for a
|
||||
package installed with a local label or as a pre-release, such as
|
||||
`torch==2.5.1` beside an installed `2.5.1+cu124`, was skipped with
|
||||
`Failed to restore`. Such an entry is now applied: that installed version
|
||||
meets it, and one that does not, such as `2.6.0+cu124`, is replaced with
|
||||
`pip install`. `~=` is a compatible-release clause: it was taken as met by
|
||||
any other version, and as not met by the same version.
|
||||
|
||||
## [4.3] - 2026-09-19
|
||||
|
||||
|
||||
@@ -118,17 +118,16 @@ class StrictVersion:
|
||||
self.parse_version_string()
|
||||
|
||||
def parse_version_string(self):
|
||||
parts = self.version_string.split('.')
|
||||
if not parts:
|
||||
raise ValueError("Version string must not be empty")
|
||||
# A pre-release follows the third number after '.' or '-': '0.1.3.post1', '1.8.0-beta.1'.
|
||||
# __str__ writes it with '-', so a version that was already normalized is read as well.
|
||||
m = re.fullmatch(r'(\d+)(?:\.(\d+)(?:\.(\d+)(?:[.-](.+))?)?)?', self.version_string)
|
||||
if m is None:
|
||||
raise ValueError(f"Invalid version: {self.version_string!r}")
|
||||
|
||||
self.major = int(parts[0])
|
||||
self.minor = int(parts[1]) if len(parts) > 1 else 0
|
||||
self.patch = int(parts[2]) if len(parts) > 2 else 0
|
||||
|
||||
# Handling pre-release versions if present
|
||||
if len(parts) > 3:
|
||||
self.pre_release = parts[3]
|
||||
self.major = int(m.group(1))
|
||||
self.minor = int(m.group(2) or 0)
|
||||
self.patch = int(m.group(3) or 0)
|
||||
self.pre_release = m.group(4)
|
||||
|
||||
def __str__(self):
|
||||
version = f"{self.major}.{self.minor}.{self.patch}"
|
||||
@@ -515,7 +514,7 @@ def parse_requirement_line(line):
|
||||
res['operator'] = operator
|
||||
|
||||
if version is not None:
|
||||
res['version'] = StrictVersion(version)
|
||||
res['version'] = version
|
||||
|
||||
if index_url is not None:
|
||||
res['index_url'] = index_url
|
||||
@@ -685,23 +684,14 @@ class PIPFixer:
|
||||
normalized_name = normalize_pip_name(parsed['package'])
|
||||
if normalized_name in new_pip_versions:
|
||||
if 'version' in parsed and 'operator' in parsed:
|
||||
cur = StrictVersion(new_pip_versions[normalized_name])
|
||||
dest = parsed['version']
|
||||
op = parsed['operator']
|
||||
if cur == dest:
|
||||
if op in ['==', '>=', '<=']:
|
||||
need_to_reinstall = False
|
||||
elif cur < dest:
|
||||
if op in ['<=', '<', '~=', '!=']:
|
||||
need_to_reinstall = False
|
||||
elif cur > dest:
|
||||
if op in ['>=', '>', '~=', '!=']:
|
||||
need_to_reinstall = False
|
||||
specifier = Specifier(parsed['operator'] + parsed['version'])
|
||||
if specifier.contains(new_pip_versions[normalized_name], prereleases=True):
|
||||
need_to_reinstall = False
|
||||
|
||||
if need_to_reinstall:
|
||||
cmd_args = ['install']
|
||||
if 'version' in parsed and 'operator' in parsed:
|
||||
cmd_args.append(parsed['package']+parsed['operator']+parsed['version'].version_string)
|
||||
cmd_args.append(parsed['package']+parsed['operator']+parsed['version'])
|
||||
|
||||
if 'index_url' in parsed:
|
||||
cmd_args.append('--index-url')
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Versions with a suffix are read: node pack versions, and pip versions in pip_auto_fix.list (#3431).
|
||||
|
||||
A node pack installed from the registry has its version in pyproject.toml. The registry holds
|
||||
versions such as `1.8.0-beta.1`, and a pack may write `0.1.3.post1`. pip reports versions such
|
||||
as `2.5.1+cu124` and `4.41.0rc1`.
|
||||
"""
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from _install_flags_testutil import import_context, import_reader
|
||||
from test_pip_fixer_opencv import fix_broken # noqa: F401 -- fixture
|
||||
|
||||
|
||||
@pytest.fixture(params=["glob", "legacy"])
|
||||
def resolve(request, tmp_path):
|
||||
"""Returns what the installed-pack scan of each core reads for a registry pack of a given version."""
|
||||
import_context()
|
||||
manager = import_reader(request.param).UnifiedManager()
|
||||
manager.cnr_map["some-pack"] = {"id": "some-pack"}
|
||||
|
||||
def run(version):
|
||||
path = tmp_path / "some-pack"
|
||||
path.mkdir(exist_ok=True)
|
||||
(path / ".tracking").write_text("", encoding="utf-8")
|
||||
(path / "pyproject.toml").write_text(
|
||||
f'[project]\nname = "some-pack"\nversion = "{version}"\n', encoding="utf-8")
|
||||
return manager.resolve_from_path(str(path))
|
||||
|
||||
return run
|
||||
|
||||
|
||||
@pytest.mark.parametrize("written, read", [
|
||||
("1.2.3", "1.2.3"),
|
||||
("2.5", "2.5.0"),
|
||||
("3", "3.0.0"),
|
||||
("0.7.2-b2", "0.7.2-b2"),
|
||||
("1.8.0-beta.1", "1.8.0-beta.1"),
|
||||
("2.0.0-inprogress", "2.0.0-inprogress"),
|
||||
("0.1.3.post1", "0.1.3-post1"),
|
||||
("0.1.3-post1", "0.1.3-post1"), # what the scan itself writes for 0.1.3.post1
|
||||
])
|
||||
def test_a_registry_pack_is_read_with_its_version(resolve, written, read):
|
||||
info = resolve(written)
|
||||
|
||||
assert info["id"] == "some-pack"
|
||||
assert info["ver"] == read
|
||||
|
||||
|
||||
@pytest.mark.parametrize("written", ["", "abc", "1.x", "1.2.x", "1.2.3-", "2.5-beta", "1.0.0rc1", "1.0.3+local"])
|
||||
def test_a_pack_without_a_readable_version_is_not_a_registry_pack(resolve, written):
|
||||
assert resolve(written) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("installed, listed", [
|
||||
("2.5.1+cu124", "torch==2.5.1"),
|
||||
("2.5.1+cu124", "torch>=2.5.0"),
|
||||
("2.5.1+cu124", "torch<=2.5.1"),
|
||||
("2.5.1+cu124", "torch~=2.5.0"),
|
||||
("2.5.1", "torch~=2.5.1"),
|
||||
("2.6.0.dev20241112", "torch>2.5.1"),
|
||||
("2.5.1rc1", "torch!=2.5.1"),
|
||||
])
|
||||
def test_a_listed_requirement_that_the_installed_version_meets_is_left_alone(fix_broken, caplog, installed, listed): # noqa: F811
|
||||
with caplog.at_level(logging.ERROR):
|
||||
commands = fix_broken({"torch": installed, "torchvision": "0.20.1", "torchaudio": "2.5.1"}, pip_auto_fix=listed + "\n")
|
||||
|
||||
assert commands == []
|
||||
assert "Failed to restore" not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("installed, listed", [
|
||||
("2.5.0+cu124", "torch==2.5.1"),
|
||||
("2.5.1rc1", "torch>=2.5.1"),
|
||||
("2.5.1rc1", "torch<2.5.1"), # as pip reads it: `<` leaves out the pre-releases of that version
|
||||
("2.5.1+cu124", "torch<2.5.1"),
|
||||
("2.5.1+cu124", "torch!=2.5.1"),
|
||||
("2.6.0", "torch~=2.5.1"),
|
||||
("2.5.0", "torch~=2.5.1"),
|
||||
])
|
||||
def test_a_listed_requirement_that_the_installed_version_misses_is_installed(fix_broken, installed, listed): # noqa: F811
|
||||
assert fix_broken({"torch": installed, "torchvision": "0.20.1", "torchaudio": "2.5.1"},
|
||||
pip_auto_fix=listed + "\n") == ["install " + listed]
|
||||
|
||||
|
||||
def test_the_index_url_of_a_listed_requirement_is_passed_to_pip(fix_broken): # noqa: F811
|
||||
index = "https://download.pytorch.org/whl/cu124"
|
||||
|
||||
assert fix_broken({"torch": "2.5.0+cu124", "torchvision": "0.20.1", "torchaudio": "2.5.1"},
|
||||
pip_auto_fix=f"torch==2.5.1 --index-url {index}\n") == [f"install torch==2.5.1 --index-url {index}"]
|
||||
Reference in New Issue
Block a user