diff --git a/libmamba/src/specs/match_spec.cpp b/libmamba/src/specs/match_spec.cpp index 8652bfa21e..2cfe2dd0a5 100644 --- a/libmamba/src/specs/match_spec.cpp +++ b/libmamba/src/specs/match_spec.cpp @@ -141,9 +141,10 @@ namespace mamba::specs /* spec= */ str.substr(spec_pos + 1), }; } + assert(spec_pos >= ns_pos + 1); return { /* channel= */ str.substr(0, ns_pos), - /* namespace= */ str.substr(ns_pos + 1, spec_pos), + /* namespace= */ str.substr(ns_pos + 1, spec_pos - ns_pos - 1), /* spec= */ str.substr(spec_pos + 1), }; } diff --git a/libmamba/tests/src/specs/test_match_spec.cpp b/libmamba/tests/src/specs/test_match_spec.cpp index 440ec65aea..38d71464a5 100644 --- a/libmamba/tests/src/specs/test_match_spec.cpp +++ b/libmamba/tests/src/specs/test_match_spec.cpp @@ -54,6 +54,14 @@ TEST_SUITE("specs::match_spec") CHECK_EQ(ms.str(), "numpy=1.7"); } + SUBCASE("conda-forge:pypi:xtensor==0.12.3") + { + auto ms = MatchSpec::parse("conda-forge:pypi:xtensor==0.12.3").value(); + CHECK_EQ(ms.name().str(), "xtensor"); + CHECK_EQ(ms.channel().value().str(), "conda-forge"); + CHECK_EQ(ms.name_space(), "pypi"); + } + SUBCASE("conda-forge/linux-64::xtensor==0.12.3") { auto ms = MatchSpec::parse("numpy[version='1.7|1.8']").value(); diff --git a/libmambapy/src/libmambapy/bindings/specs.cpp b/libmambapy/src/libmambapy/bindings/specs.cpp index b0084c1a23..4c4623bf59 100644 --- a/libmambapy/src/libmambapy/bindings/specs.cpp +++ b/libmambapy/src/libmambapy/bindings/specs.cpp @@ -12,6 +12,8 @@ #include "mamba/specs/authentication_info.hpp" #include "mamba/specs/channel.hpp" #include "mamba/specs/conda_url.hpp" +#include "mamba/specs/error.hpp" +#include "mamba/specs/glob_spec.hpp" #include "mamba/specs/match_spec.hpp" #include "mamba/specs/package_info.hpp" #include "mamba/specs/platform.hpp" @@ -35,6 +37,8 @@ namespace mambapy namespace py = pybind11; using namespace mamba::specs; + py::register_local_exception(m, "ParseError", PyExc_ValueError); + m.def("archive_extensions", []() { return ARCHIVE_EXTENSIONS; }); m.def( @@ -323,6 +327,7 @@ namespace mambapy py::arg("platform_filters"), py::arg("type") = UnresolvedChannel::Type::Unknown ) + .def("__str__", &UnresolvedChannel::str) .def("__copy__", ©) .def("__deepcopy__", &deepcopy, py::arg("memo")) .def_property_readonly("type", &UnresolvedChannel::type) @@ -694,11 +699,36 @@ namespace mambapy .def("__copy__", ©) .def("__deepcopy__", &deepcopy, py::arg("memo")); - // WIP MatchSpec class + py::class_(m, "GlobSpec") + .def_readonly_static("free_pattern", &GlobSpec::free_pattern) + .def_readonly_static("glob_pattern", &GlobSpec::glob_pattern) + .def(py::init<>()) + .def(py::init(), py::arg("spec")) + .def("contains", &GlobSpec::contains) + .def("is_free", &GlobSpec::is_free) + .def("is_exact", &GlobSpec::is_exact) + .def("__str__", &GlobSpec::str) + .def("__copy__", ©) + .def("__deepcopy__", &deepcopy, py::arg("memo")); + py::class_(m, "MatchSpec") + .def_property_readonly_static("NameSpec", &py::type::of) + .def_property_readonly_static("BuildStringSpec", &py::type::of) + .def_readonly_static("url_md5_sep", &MatchSpec::url_md5_sep) + .def_readonly_static("prefered_list_open", &MatchSpec::prefered_list_open) + .def_readonly_static("prefered_list_close", &MatchSpec::prefered_list_close) + .def_readonly_static("alt_list_open", &MatchSpec::alt_list_open) + .def_readonly_static("alt_list_close", &MatchSpec::alt_list_close) + .def_readonly_static("prefered_quote", &MatchSpec::prefered_quote) + .def_readonly_static("alt_quote", &MatchSpec::alt_quote) + .def_readonly_static("channel_namespace_spec_sep", &MatchSpec::channel_namespace_spec_sep) + .def_readonly_static("attribute_sep", &MatchSpec::attribute_sep) + .def_readonly_static("attribute_assign", &MatchSpec::attribute_assign) + .def_readonly_static("package_version_sep", &MatchSpec::package_version_sep) .def_static("parse", &MatchSpec::parse) + .def_static("parse_url", &MatchSpec::parse_url) .def( - // Hard deperecation since errors would be hard to track. + // V2 Migation: Hard deperecation since errors would be hard to track. py::init( [](std::string_view) -> MatchSpec { throw std::invalid_argument( @@ -708,6 +738,23 @@ namespace mambapy ), py::arg("spec") ) + .def_property("channel", &MatchSpec::channel, &MatchSpec::set_channel) + .def_property("filename", &MatchSpec::filename, &MatchSpec::set_filename) + .def_property("subdirs", &MatchSpec::subdirs, &MatchSpec::set_subdirs) + .def_property("name_space", &MatchSpec::name_space, &MatchSpec::set_name_space) + .def_property("name", &MatchSpec::name, &MatchSpec::set_name) + .def_property("version", &MatchSpec::version, &MatchSpec::set_version) + .def_property("build_number", &MatchSpec::build_number, &MatchSpec::set_build_number) + .def_property("build_string", &MatchSpec::build_string, &MatchSpec::set_build_string) + .def_property("md5", &MatchSpec::md5, &MatchSpec::set_md5) + .def_property("sha256", &MatchSpec::sha256, &MatchSpec::set_sha256) + .def_property("license", &MatchSpec::license, &MatchSpec::set_license) + .def_property("license_family", &MatchSpec::license_family, &MatchSpec::set_license_family) + .def_property("features", &MatchSpec::features, &MatchSpec::set_features) + .def_property("track_features", &MatchSpec::track_features, &MatchSpec::set_track_features) + .def_property("optional", &MatchSpec::optional, &MatchSpec::set_optional) + .def("is_file", &MatchSpec::is_file) + .def("is_simple", &MatchSpec::is_simple) .def("conda_build_form", &MatchSpec::conda_build_form) .def("__str__", &MatchSpec::str) .def("__copy__", ©) diff --git a/libmambapy/tests/test_specs.py b/libmambapy/tests/test_specs.py index 58d53c67f2..92d4870ddb 100644 --- a/libmambapy/tests/test_specs.py +++ b/libmambapy/tests/test_specs.py @@ -19,6 +19,10 @@ def test_import_recursive(): _p = mamba.specs.Platform.noarch +def test_ParseError(): + assert issubclass(libmambapy.specs.ParseError, ValueError) + + def test_archive_extension(): assert libmambapy.specs.archive_extensions() == [".tar.bz2", ".conda", ".whl"] @@ -173,6 +177,10 @@ def test_CondaURL_setters(): def test_CondaURL_parse(): CondaURL = libmambapy.specs.CondaURL + # Errors + with pytest.raises(libmambapy.specs.ParseError): + CondaURL.parse("py>#") + url = CondaURL.parse( "https://user%40mail.com:pas%23@repo.mamba.pm:400/t/xy-12345678-1234/%20conda/linux-64/pkg.conda" ) @@ -262,12 +270,20 @@ def test_UnresolvedChannel(): uc = UnresolvedChannel(location="conda-forge", platform_filters=set(), type="Name") assert uc.type == UnresolvedChannel.Type.Name + # str + uc = UnresolvedChannel(location="conda-forge", platform_filters=set(), type="Name") + assert str(uc) == "conda-forge" + # Parser uc = UnresolvedChannel.parse("conda-forge[linux-64]") assert uc.location == "conda-forge" assert uc.platform_filters == {"linux-64"} assert uc.type == UnresolvedChannel.Type.Name + # Errors + with pytest.raises(libmambapy.specs.ParseError): + UnresolvedChannel.parse("conda-forge]") + # Copy other = copy.deepcopy(uc) assert other.location == uc.location @@ -612,6 +628,10 @@ def test_Version(): # Parse v = Version.parse("3!1.3ab2.4+42.0alpha") + # Errors + with pytest.raises(libmambapy.specs.ParseError): + Version.parse("#!33") + # Getters assert v.epoch == 3 assert v.version == CommonVersion( @@ -678,6 +698,10 @@ def test_VersionSpec(): vs = VersionSpec.parse(">2.0,<3.0") + # Errors + with pytest.raises(libmambapy.specs.ParseError): + VersionSpec.parse("=2,") + assert not vs.contains(Version.parse("1.1")) assert vs.contains(Version.parse("2.1")) @@ -778,13 +802,72 @@ def test_PackageInfo_V2Migrator(): pkg.url = "https://repo.mamba.pm/conda-forge/linux-64/foo-4.0-mybld.conda" +def test_GlobSpec(): + GlobSpec = libmambapy.specs.GlobSpec + spec = libmambapy.specs.GlobSpec("py*") + + assert GlobSpec().is_free() + assert not spec.is_free() + + assert GlobSpec("python").is_exact() + assert not spec.is_exact() + + assert spec.contains("python") + + assert str(spec) == "py*" + + # Copy + other = copy.deepcopy(spec) + assert str(other) == str(spec) + assert other is not spec + + def test_MatchSpec(): MatchSpec = libmambapy.specs.MatchSpec - ms = MatchSpec.parse("conda-forge::python=3.7=*pypy") + # Errors + with pytest.raises(libmambapy.specs.ParseError): + MatchSpec.parse_url("httos:/") + + ms = MatchSpec.parse_url("https://conda.com/pkg-2-bld.conda") + assert ms.is_file() + assert str(ms.name) == "pkg" + assert ms.filename == "pkg-2-bld.conda" + + # Errors + with pytest.raises(libmambapy.specs.ParseError): + MatchSpec.parse("py>#") + + ms = MatchSpec.parse( + "conda-forge[plat]:ns:python=3.7=*pypy" + "[md5=m,sha256=s,license=l, license_family=lf,track_features=ft,optional]" + ) + + assert str(ms.channel) == "conda-forge[plat]" + assert ms.subdirs == {"plat"} + assert ms.name_space == "ns" + assert str(ms.name) == "python" + assert str(ms.version) == "=3.7" + assert str(ms.build_string) == "*pypy" + assert ms.md5 == "m" + assert ms.sha256 == "s" + assert ms.license == "l" + assert ms.license_family == "lf" + assert ms.track_features == "ft" + assert ms.optional + assert not ms.is_file() + assert not ms.is_simple() # str - assert str(ms) == "conda-forge::python=3.7[build='*pypy']" + assert str(ms) == ( + "conda-forge[plat]::python=3.7" + "[build='*pypy',track_features=ft,md5=m,sha256=s,license=l,license_family=lf,optional]" + ) + + # Copy + other = copy.deepcopy(ms) + assert str(other) == str(ms) + assert other is not ms def test_MatchSpec_V2Migrator():