diff --git a/conda_forge_tick/update_sources.py b/conda_forge_tick/update_sources.py index 32d621911..4bad98673 100644 --- a/conda_forge_tick/update_sources.py +++ b/conda_forge_tick/update_sources.py @@ -2,11 +2,13 @@ import collections.abc import copy import functools +import json import logging import re import subprocess import typing import urllib.parse +from pathlib import Path from typing import Iterator, List, Optional import feedparser @@ -708,3 +710,46 @@ def get_url(self, meta_yaml) -> Optional[str]: def get_version(self, url: str) -> Optional[str]: return url # = next version, same as in BaseRawURL + + +class CratesIO(AbstractSource): + name = "CratesIO" + + def get_url(self, meta_yaml) -> Optional[str]: + if "crates.io" not in meta_yaml["url"]: + return None + + pkg = Path(meta_yaml["url"]).parts[5] + tier = self._tier_directory(pkg) + + return f"https://index.crates.io/{tier}" + + def get_version(self, url: str) -> Optional[str]: + r = requests.get(url) + + if not r.ok: + return None + + # the response body is a newline-delimited JSON stream, with the latest version + # being the last line + latest = json.loads(r.text.splitlines()[-1]) + + return latest.get("vers") + + @staticmethod + def _tier_directory(package: str) -> str: + """Depending on the length of the package name, the tier directory structure + will differ. + Documented here: https://doc.rust-lang.org/cargo/reference/registry-index.html#index-files + """ + if not package: + raise ValueError("Package name cannot be empty") + + name_len = len(package) + + if name_len <= 2: + return f"{name_len}/{package}" + elif name_len == 3: + return f"{name_len}/{package[0]}/{package}" + else: + return f"{package[0:2]}/{package[2:4]}/{package}" diff --git a/conda_forge_tick/update_upstream_versions.py b/conda_forge_tick/update_upstream_versions.py index 931ab4aa7..0e2052622 100644 --- a/conda_forge_tick/update_upstream_versions.py +++ b/conda_forge_tick/update_upstream_versions.py @@ -35,6 +35,7 @@ NPM, NVIDIA, AbstractSource, + CratesIO, Github, GithubReleases, IncrementAlphaRawURL, @@ -418,6 +419,7 @@ def all_version_sources(): return ( PyPI(), CRAN(), + CratesIO(), NPM(), ROSDistro(), RawURL(), diff --git a/tests/test_update_sources.py b/tests/test_update_sources.py new file mode 100644 index 000000000..76bf86a99 --- /dev/null +++ b/tests/test_update_sources.py @@ -0,0 +1,91 @@ +from unittest.mock import Mock, patch + +import pytest + +from conda_forge_tick.update_sources import CratesIO + + +class TestCratesIOTierDirectory: + def test_four_or_more_characters(self): + pkg = "rasusa" + + actual = CratesIO._tier_directory(pkg) + expected = "ra/su/rasusa" + + assert actual == expected + + def test_four_characters(self): + pkg = "psdm" + + actual = CratesIO._tier_directory(pkg) + expected = "ps/dm/psdm" + + assert actual == expected + + def test_three_characters(self): + pkg = "syn" + + actual = CratesIO._tier_directory(pkg) + expected = "3/s/syn" + + assert actual == expected + + def test_two_characters(self): + pkg = "it" + + actual = CratesIO._tier_directory(pkg) + expected = "2/it" + + assert actual == expected + + def test_one_character(self): + pkg = "a" + + actual = CratesIO._tier_directory(pkg) + expected = "1/a" + + assert actual == expected + + def test_empty_string(self): + pkg = "" + + with pytest.raises(ValueError): + CratesIO._tier_directory(pkg) + + +class TestCratesIOGetVersion: + def test_valid_package(self): + # as far as I can tell, this package has not had a new version in the last 9 + # years, so it should be safe to use for testing as we don't expect the version + # to change + pkg = "gopher" + tier = CratesIO._tier_directory(pkg) + url = f"https://index.crates.io/{tier}" + + actual = CratesIO().get_version(url) + expected = "0.0.3" + + assert actual == expected + + def test_invalid_package(self): + pkg = "shdfbshbvjhbvhsbhsb" + tier = CratesIO._tier_directory(pkg) + url = f"https://index.crates.io/{tier}" + + result = CratesIO().get_version(url) + assert result is None + + @patch("conda_forge_tick.update_sources.requests.get") + def test_empty_package(self, mock_get): + pkg = "syn" + tier = CratesIO._tier_directory(pkg) + url = f"https://index.crates.io/{tier}" + + # Mock response + mock_response = Mock() + mock_response.ok = True + mock_response.text = '{"name": "syn"}' + mock_get.return_value = mock_response + + result = CratesIO().get_version(url) + assert result is None diff --git a/tests/test_upstream_versions.py b/tests/test_upstream_versions.py index f15e0ca71..1bc587db6 100644 --- a/tests/test_upstream_versions.py +++ b/tests/test_upstream_versions.py @@ -17,6 +17,7 @@ NPM, NVIDIA, AbstractSource, + CratesIO, Github, GithubReleases, PyPI, @@ -1254,6 +1255,7 @@ def test_update_upstream_versions_no_packages_to_update( default_sources = ( "PyPI", "CRAN", + "CratesIO", "NPM", "ROSDistro", "RawURL", @@ -1718,3 +1720,37 @@ def test_github_releases(tmpdir, url, feedstock_version): ghr = GithubReleases() url = ghr.get_url(meta_yaml) assert VersionOrder(ghr.get_version(url)) > VersionOrder(feedstock_version) + + +def test_latest_version_cratesio(tmpdir): + name = "wbg-rand" + recipe_path = os.path.join(YAML_PATH, "version_wbg-rand.yaml") + curr_ver = "0.4.0" + ver = "0.4.1" + source = CratesIO() + + with open(recipe_path) as fd: + inp = fd.read() + + pmy = LazyJson(os.path.join(str(tmpdir), "cf-scripts-test.json")) + with pmy as _pmy: + yml = parse_meta_yaml(inp) + _pmy.update(yml["source"]) + _pmy.update( + { + "feedstock_name": name, + "version": curr_ver, + "raw_meta_yaml": inp, + "meta_yaml": yml, + }, + ) + + attempt = get_latest_version(name, pmy, [source], use_container=False) + if ver is None: + assert attempt["new_version"] is not False + assert attempt["new_version"] != curr_ver + assert VersionOrder(attempt["new_version"]) > VersionOrder(curr_ver) + elif ver is False: + assert attempt["new_version"] is ver + else: + assert ver == attempt["new_version"] diff --git a/tests/test_yaml/version_wbg-rand.yaml b/tests/test_yaml/version_wbg-rand.yaml new file mode 100644 index 000000000..9814429be --- /dev/null +++ b/tests/test_yaml/version_wbg-rand.yaml @@ -0,0 +1,37 @@ +package: + name: wbg-rand + version: "0.4.0" + +source: + url: https://crates.io/api/v1/crates/wbg-rand/0.4.0/download + fn: "wbg-rand-0.4.0.tar.gz" + sha256: 5505e10cb191f56fed835c35baf4ac97b5466148a13fbcaeb1173198b1a52b4c + +build: + number: 0 + skip: true # [win] + +requirements: + build: + - {{ compiler('rust') }} + - {{ compiler('c') }} + - {{ stdlib("c") }} + - cargo-bundle-licenses + +test: + commands: + - echo "This is a placeholder for the test section" + +about: + home: https://github.com/alexcrichton/wbg-rand + summary: 'Random numbers for wasm32-unknown-unknown in Rust' + description: | + Implementation of rand for wasm32-unknown-unknown in Rust using #[wasm_bindgen]. + license: MIT AND Apache-2.0 + license_file: + - LICENSE-MIT + - LICENSE-APACHE + +extra: + recipe-maintainers: + - mbhall88