mirror of https://github.com/mamba-org/mamba.git
fix: Support for PEP 440 "Compatible Releases" (operator `~=` for `MatchSpec`) (#3483)
Signed-off-by: Julien Jerphanion <git@jjerphan.xyz> Co-authored-by: Johan Mabille <johan.mabille@gmail.com>
This commit is contained in:
parent
2e8b3ee7bc
commit
0b824fe252
|
@ -505,6 +505,10 @@ namespace mamba::specs
|
|||
std::string raw_match_spec_str = std::string(str);
|
||||
raw_match_spec_str = util::strip(raw_match_spec_str);
|
||||
|
||||
// Those are temporary adaptations to handle some instances of `MatchSpec` which is not
|
||||
// yet formally specified.
|
||||
// For a tentative formulation of the MatchSpec see: https://github.com/conda/ceps/pull/82
|
||||
|
||||
// Remove any with space after binary operators, such as:
|
||||
// - `openmpi-4.1.4-ha1ae619_102`'s improperly encoded `constrains`: "cudatoolkit >= 10.2"
|
||||
// - `pytorch-1.13.0-cpu_py310h02c325b_0.conda`'s improperly encoded
|
||||
|
@ -516,7 +520,7 @@ namespace mamba::specs
|
|||
// TODO: this solution reallocates memory several times potentially, but the
|
||||
// number of operators is small and the strings are short, so it must be fine.
|
||||
// If needed it can be optimized so that the string is only copied once.
|
||||
for (const std::string& op : { ">=", "<=", "==", ">", "<", "!=", "=", "==", "," })
|
||||
for (const std::string& op : { ">=", "<=", "==", ">", "<", "!=", "=", "==", "~=", "," })
|
||||
{
|
||||
const std::string& bad_op = op + " ";
|
||||
while (raw_match_spec_str.find(bad_op) != std::string::npos)
|
||||
|
@ -528,6 +532,45 @@ namespace mamba::specs
|
|||
}
|
||||
}
|
||||
|
||||
// Handle PEP 440 "Compatible release" specification
|
||||
// See: https://peps.python.org/pep-0440/#compatible-release
|
||||
//
|
||||
// Find a general replacement of the encoding of `~=` with `>=,.*` to be able to parse it
|
||||
// properly.
|
||||
//
|
||||
// For instance:
|
||||
//
|
||||
// "~=x.y" must be replaced to ">=x.y,x.*" where `x` and `y` are positive integers.
|
||||
//
|
||||
// This solution must handle the case where the version is encoded with `~=` within the
|
||||
// specification for instance:
|
||||
//
|
||||
// ">1.8,<2|==1.7,!=1.9,~=1.7.1 py34_0"
|
||||
//
|
||||
// must be replaced with:
|
||||
//
|
||||
// ">1.8,<2|==1.7,!=1.9,>=1.7.1,1.7.* py34_0"
|
||||
//
|
||||
while (raw_match_spec_str.find("~=") != std::string::npos)
|
||||
{
|
||||
// Extract the string before the `~=` operator (">1.8,<2|==1.7,!=1.9," for the above
|
||||
// example)
|
||||
const auto before = raw_match_spec_str.substr(0, str.find("~="));
|
||||
// Extract the string after the `~=` operator (include `~=` in it) and the next operator
|
||||
// space or end of the string ("~=1.7.1 py34_0" for the above example)
|
||||
const auto after = raw_match_spec_str.substr(str.find("~="));
|
||||
// Extract the version part after the `~=` operator ("1.7.1" for the above example)
|
||||
const auto version = after.substr(2, after.find_first_of(" ,") - 2);
|
||||
// Extract the version part without the last segment ("1.7" for the above example)
|
||||
const auto version_without_last_segment = version.substr(0, version.find_last_of('.'));
|
||||
// Extract the build part after the version part (" py34_0" for the above example) if
|
||||
// present
|
||||
const auto build = after.find(" ") != std::string::npos ? after.substr(after.find(" "))
|
||||
: "";
|
||||
raw_match_spec_str = before + ">=" + version + "," + version_without_last_segment + ".*"
|
||||
+ build;
|
||||
}
|
||||
|
||||
auto parse_error = [&raw_match_spec_str](std::string_view err) -> tl::unexpected<ParseError>
|
||||
{
|
||||
return tl::make_unexpected(ParseError(
|
||||
|
|
|
@ -446,9 +446,28 @@ TEST_SUITE("specs::match_spec")
|
|||
{
|
||||
auto ms = MatchSpec::parse(R"(numpy >1.8,<2|==1.7,!=1.9,~=1.7.1 py34_0)").value();
|
||||
CHECK_EQ(ms.name().str(), "numpy");
|
||||
CHECK_EQ(ms.version().str(), ">1.8,((<2|==1.7),(!=1.9,~=1.7))");
|
||||
CHECK_EQ(ms.version().str(), ">1.8,((<2|==1.7),(!=1.9,(>=1.7.1,=1.7)))");
|
||||
CHECK_EQ(ms.build_string().str(), "py34_0");
|
||||
CHECK_EQ(ms.str(), R"ms(numpy[version=">1.8,((<2|==1.7),(!=1.9,~=1.7))",build="py34_0"])ms");
|
||||
CHECK_EQ(
|
||||
ms.str(),
|
||||
R"ms(numpy[version=">1.8,((<2|==1.7),(!=1.9,(>=1.7.1,=1.7)))",build="py34_0"])ms"
|
||||
);
|
||||
}
|
||||
|
||||
SUBCASE("python-graphviz~=0.20")
|
||||
{
|
||||
auto ms = MatchSpec::parse("python-graphviz~=0.20").value();
|
||||
CHECK_EQ(ms.name().str(), "python-graphviz");
|
||||
CHECK_EQ(ms.version().str(), ">=0.20,=0");
|
||||
CHECK_EQ(ms.str(), R"ms(python-graphviz[version=">=0.20,=0"])ms");
|
||||
}
|
||||
|
||||
SUBCASE("python-graphviz ~= 0.20")
|
||||
{
|
||||
auto ms = MatchSpec::parse("python-graphviz ~= 0.20").value();
|
||||
CHECK_EQ(ms.name().str(), "python-graphviz");
|
||||
CHECK_EQ(ms.version().str(), ">=0.20,=0");
|
||||
CHECK_EQ(ms.str(), R"ms(python-graphviz[version=">=0.20,=0"])ms");
|
||||
}
|
||||
|
||||
SUBCASE("*[md5=fewjaflknd]")
|
||||
|
|
|
@ -4,6 +4,7 @@ import shutil
|
|||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from packaging.version import Version
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -633,6 +634,14 @@ class TestInstall:
|
|||
reinstall_res = helpers.install("xtensor", "--force-reinstall", "--json")
|
||||
assert "xtensor" in {pkg["name"] for pkg in reinstall_res["actions"]["LINK"]}
|
||||
|
||||
def test_install_compatible_release(self, existing_cache):
|
||||
"""Install compatible release."""
|
||||
res = helpers.install("numpy~=1.26.0", "--force-reinstall", "--json")
|
||||
assert "numpy" in {pkg["name"] for pkg in res["actions"]["LINK"]}
|
||||
|
||||
numpy = [pkg for pkg in res["actions"]["LINK"] if pkg["name"] == "numpy"][0]
|
||||
assert Version(numpy["version"]) >= Version("1.26.0")
|
||||
|
||||
|
||||
def test_install_check_dirs(tmp_home, tmp_root_prefix):
|
||||
env_name = "myenv"
|
||||
|
|
Loading…
Reference in New Issue