diff --git a/conda_forge_tick/hashing.py b/conda_forge_tick/hashing.py index dd6649536..a8ab1c9a6 100644 --- a/conda_forge_tick/hashing.py +++ b/conda_forge_tick/hashing.py @@ -2,12 +2,18 @@ import hashlib import math import time -from multiprocessing import Pipe, Process +from multiprocessing import Pipe, Process, connection import requests -def _hash_url(url, hash_type, progress=False, conn=None, timeout=None): +def _hash_url( + url: str, + hash_type: str, + progress: bool = False, + conn: connection.Connection | None = None, + timeout: int | None = None, +) -> str | None: _hash = None try: ha = getattr(hashlib, hash_type)() @@ -68,7 +74,12 @@ def _hash_url(url, hash_type, progress=False, conn=None, timeout=None): @functools.lru_cache(maxsize=1024) -def hash_url(url, timeout=None, progress=False, hash_type="sha256"): +def hash_url( + url: str, + timeout: int | None = None, + progress: bool = False, + hash_type: str = "sha256", +) -> str | None: """Hash a url with a timeout. Parameters diff --git a/conda_forge_tick/migrators/core.py b/conda_forge_tick/migrators/core.py index 48f0c014b..de18203c4 100644 --- a/conda_forge_tick/migrators/core.py +++ b/conda_forge_tick/migrators/core.py @@ -5,6 +5,7 @@ import logging import re import typing +from pathlib import Path from typing import Any, List, Sequence, Set import dateutil.parser @@ -14,7 +15,7 @@ from conda_forge_tick.lazy_json_backends import LazyJson from conda_forge_tick.make_graph import make_outputs_lut_from_graph from conda_forge_tick.path_lengths import cyclic_topological_sort -from conda_forge_tick.update_recipe import update_build_number +from conda_forge_tick.update_recipe import update_build_number, v2 from conda_forge_tick.utils import ( frozen_to_json_friendly, get_bot_run_url, @@ -560,7 +561,7 @@ def order( } return cyclic_topological_sort(graph, top_level) - def set_build_number(self, filename: str) -> None: + def set_build_number(self, filename: str | Path) -> None: """Bump the build number of the specified recipe. Parameters @@ -568,17 +569,19 @@ def set_build_number(self, filename: str) -> None: filename : str Path the the meta.yaml """ - with open(filename) as f: - raw = f.read() + filename = Path(filename) + if filename.name == "recipe.yaml": + filename.write_text(v2.update_build_number(filename, self.new_build_number)) + else: + raw = filename.read_text() - new_myaml = update_build_number( - raw, - self.new_build_number, - build_patterns=self.build_patterns, - ) + new_myaml = update_build_number( + raw, + self.new_build_number, + build_patterns=self.build_patterns, + ) - with open(filename, "w") as f: - f.write(new_myaml) + filename.write_text(new_myaml) def new_build_number(self, old_number: int) -> int: """Determine the new build number to use. diff --git a/conda_forge_tick/migrators/version.py b/conda_forge_tick/migrators/version.py index 9cf8b8186..afd500803 100644 --- a/conda_forge_tick/migrators/version.py +++ b/conda_forge_tick/migrators/version.py @@ -5,6 +5,7 @@ import random import typing import warnings +from pathlib import Path from typing import Any, List, Sequence import conda.exceptions @@ -14,9 +15,8 @@ from conda_forge_tick.contexts import FeedstockContext from conda_forge_tick.migrators.core import Migrator from conda_forge_tick.models.pr_info import MigratorName -from conda_forge_tick.os_utils import pushd from conda_forge_tick.update_deps import get_dep_updates_and_hints -from conda_forge_tick.update_recipe import update_version +from conda_forge_tick.update_recipe import update_version, v2 from conda_forge_tick.utils import get_keys_default, sanitize_string if typing.TYPE_CHECKING: @@ -195,29 +195,37 @@ def migrate( ) -> "MigrationUidTypedDict": version = attrs["new_version"] - with open(os.path.join(recipe_dir, "meta.yaml")) as fp: - raw_meta_yaml = fp.read() + recipe_dir = Path(recipe_dir) + meta_yaml = recipe_dir / "meta.yaml" + recipe_yaml = recipe_dir / "recipe.yaml" + if meta_yaml.exists(): + raw_meta_yaml = meta_yaml.read_text() - updated_meta_yaml, errors = update_version( - raw_meta_yaml, - version, - hash_type=hash_type, - ) + updated_meta_yaml, errors = update_version( + raw_meta_yaml, + version, + hash_type=hash_type, + ) - if len(errors) == 0 and updated_meta_yaml is not None: - with pushd(recipe_dir): - with open("meta.yaml", "w") as fp: - fp.write(updated_meta_yaml) - self.set_build_number("meta.yaml") + if len(errors) == 0 and updated_meta_yaml is not None: + meta_yaml.write_text(updated_meta_yaml) + self.set_build_number(meta_yaml) - return super().migrate(recipe_dir, attrs) - else: + elif recipe_yaml.exists(): + updated_recipe, errors = v2.update_version(recipe_yaml, version) + if len(errors) == 0 and updated_recipe is not None: + recipe_yaml.write_text(updated_recipe) + self.set_build_number(recipe_yaml) + + if len(errors) != 0: raise VersionMigrationError( _fmt_error_message( errors, version, ) ) + else: + return super().migrate(recipe_dir, attrs) def pr_body(self, feedstock_ctx: FeedstockContext, add_label_text=False) -> str: if feedstock_ctx.feedstock_name in self.effective_graph.nodes: diff --git a/conda_forge_tick/update_recipe/v2/__init__.py b/conda_forge_tick/update_recipe/v2/__init__.py new file mode 100644 index 000000000..f097d437c --- /dev/null +++ b/conda_forge_tick/update_recipe/v2/__init__.py @@ -0,0 +1,4 @@ +from .build_number import update_build_number +from .version import update_version + +__all__ = ["update_build_number", "update_version"] diff --git a/conda_forge_tick/update_recipe/v2/build_number.py b/conda_forge_tick/update_recipe/v2/build_number.py new file mode 100644 index 000000000..d7e07d376 --- /dev/null +++ b/conda_forge_tick/update_recipe/v2/build_number.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any, Literal + +from conda_forge_tick.update_recipe.v2.yaml import _dump_yaml_to_str, _load_yaml + +if TYPE_CHECKING: + from pathlib import Path + +logger = logging.getLogger(__name__) + +HashType = Literal["md5", "sha256"] + + +def _update_build_number_in_context( + recipe: dict[str, Any], new_build_number: int +) -> bool: + for key in recipe.get("context", {}): + if key.startswith("build_") or key == "build": + recipe["context"][key] = new_build_number + return True + return False + + +def _update_build_number_in_recipe( + recipe: dict[str, Any], new_build_number: int +) -> bool: + is_modified = False + if "build" in recipe and "number" in recipe["build"]: + recipe["build"]["number"] = new_build_number + is_modified = True + + if "outputs" in recipe: + for output in recipe["outputs"]: + if "build" in output and "number" in output["build"]: + output["build"]["number"] = new_build_number + is_modified = True + + return is_modified + + +def update_build_number(file: Path, new_build_number: int = 0) -> str: + """ + Update the build number in the recipe file. + + Arguments: + ---------- + * `file` - The path to the recipe file. + * `new_build_number` - The new build number to use. (default: 0) + + Returns: + -------- + * The updated recipe as a string. + """ + data = _load_yaml(file) + build_number_modified = _update_build_number_in_context(data, new_build_number) + if not build_number_modified: + _update_build_number_in_recipe(data, new_build_number) + + return _dump_yaml_to_str(data) diff --git a/conda_forge_tick/update_recipe/v2/conditional_list.py b/conda_forge_tick/update_recipe/v2/conditional_list.py new file mode 100644 index 000000000..5b0a08cdc --- /dev/null +++ b/conda_forge_tick/update_recipe/v2/conditional_list.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Generic, List, TypeVar, Union, cast + +if TYPE_CHECKING: + from collections.abc import Callable, Generator + +T = TypeVar("T") +K = TypeVar("K") + + +class IfStatement(Generic[T]): + if_: Any + then: T | list[T] + else_: T | list[T] | None + + +ConditionalList = Union[T, IfStatement[T], List[Union[T, IfStatement[T]]]] + + +def visit_conditional_list( # noqa: C901 + value: T | IfStatement[T] | list[T | IfStatement[T]], + evaluator: Callable[[Any], bool] | None = None, +) -> Generator[T]: + """ + A function that yields individual branches of a conditional list. + + Arguments + --------- + * `value` - The value to evaluate + * `evaluator` - An optional evaluator to evaluate the `if` expression. + + Returns + ------- + A generator that yields the individual branches. + """ + + def yield_from_list(value: list[K] | K) -> Generator[K]: + if isinstance(value, list): + yield from value + else: + yield value + + if not isinstance(value, list): + value = [value] + + for element in value: + if isinstance(element, dict): + if (expr := element.get("if", None)) is not None: + then = element.get("then") + otherwise = element.get("else") + # Evaluate the if expression if the evaluator is provided + if evaluator: + if evaluator(expr): + yield from yield_from_list(then) + elif otherwise: + yield from yield_from_list(otherwise) + # Otherwise, just yield the branches + else: + yield from yield_from_list(then) + if otherwise: + yield from yield_from_list(otherwise) + else: + # In this case its not an if statement + yield cast(T, element) + # If the element is not a dictionary, just yield it + else: + # (tim) I get a pyright error here, but I don't know how to fix it + yield cast(T, element) diff --git a/conda_forge_tick/update_recipe/v2/context.py b/conda_forge_tick/update_recipe/v2/context.py new file mode 100644 index 000000000..cb86d8066 --- /dev/null +++ b/conda_forge_tick/update_recipe/v2/context.py @@ -0,0 +1,19 @@ +import jinja2 + + +def load_recipe_context( + context: dict[str, str], jinja_env: jinja2.Environment +) -> dict[str, str]: + """ + Load all string values from the context dictionary as Jinja2 templates. + Use linux-64 as default target_platform, build_platform, and mpi. + """ + # Process each key-value pair in the dictionary + for key, value in context.items(): + # If the value is a string, render it as a template + if isinstance(value, str): + template = jinja_env.from_string(value) + rendered_value = template.render(context) + context[key] = rendered_value + + return context diff --git a/conda_forge_tick/update_recipe/v2/jinja/__init__.py b/conda_forge_tick/update_recipe/v2/jinja/__init__.py new file mode 100644 index 000000000..7d3fad5a0 --- /dev/null +++ b/conda_forge_tick/update_recipe/v2/jinja/__init__.py @@ -0,0 +1,3 @@ +from .jinja import jinja_env + +__all__ = ["jinja_env"] diff --git a/conda_forge_tick/update_recipe/v2/jinja/filters.py b/conda_forge_tick/update_recipe/v2/jinja/filters.py new file mode 100644 index 000000000..132771619 --- /dev/null +++ b/conda_forge_tick/update_recipe/v2/jinja/filters.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from conda_forge_tick.update_recipe.v2.jinja.utils import _MissingUndefined + + +def _version_to_build_string(some_string: str | _MissingUndefined) -> str: + """ + Converts some version by removing the . character and returning only the first two elements of the version. + If piped value is undefined, it returns the undefined value as is. + """ + if isinstance(some_string, _MissingUndefined): + return f"{some_string._undefined_name}_version_to_build_string" # noqa: SLF001 + # We first split the string by whitespace and take the first part + split = some_string.split()[0] if some_string.split() else some_string + # We then split the string by . and take the first two parts + parts = split.split(".") + major = parts[0] if len(parts) > 0 else "" + minor = parts[1] if len(parts) > 1 else "" + return f"{major}{minor}" + + +def _bool(value: str) -> bool: + return bool(value) + + +def _split(s: str, sep: str | None = None) -> list[str]: + """Filter that split a string by a separator""" + return s.split(sep) diff --git a/conda_forge_tick/update_recipe/v2/jinja/jinja.py b/conda_forge_tick/update_recipe/v2/jinja/jinja.py new file mode 100644 index 000000000..fa3cb89ab --- /dev/null +++ b/conda_forge_tick/update_recipe/v2/jinja/jinja.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from typing import TypedDict + +import jinja2 +from jinja2.sandbox import SandboxedEnvironment + +from conda_forge_tick.update_recipe.v2.jinja.filters import ( + _bool, + _split, + _version_to_build_string, +) +from conda_forge_tick.update_recipe.v2.jinja.objects import ( + _stub_compatible_pin, + _stub_is_linux, + _stub_is_unix, + _stub_is_win, + _stub_match, + _stub_subpackage_pin, + _StubEnv, +) +from conda_forge_tick.update_recipe.v2.jinja.utils import _MissingUndefined + +# from conda_forge_tick.update_recipe.v2.loader import load_yaml + + +class RecipeWithContext(TypedDict, total=False): + context: dict[str, str] + + +def jinja_env() -> SandboxedEnvironment: + """ + Create a `rattler-build` specific Jinja2 environment with modified syntax. + Target platform, build platform, and mpi are set to linux-64 by default. + """ + env = SandboxedEnvironment( + variable_start_string="${{", + variable_end_string="}}", + trim_blocks=True, + lstrip_blocks=True, + autoescape=jinja2.select_autoescape(default_for_string=False), + undefined=_MissingUndefined, + ) + + env_obj = _StubEnv() + + # inject rattler-build recipe functions in jinja environment + env.globals.update( + { + "compiler": lambda x: x + "_compiler_stub", + "stdlib": lambda x: x + "_stdlib_stub", + "pin_subpackage": _stub_subpackage_pin, + "pin_compatible": _stub_compatible_pin, + "cdt": lambda *args, **kwargs: "cdt_stub", # noqa: ARG005 + "env": env_obj, + "match": _stub_match, + "is_unix": _stub_is_unix, + "is_win": _stub_is_win, + "is_linux": _stub_is_linux, + "unix": True, + "linux": True, + "target_platform": "linux-64", + "build_platform": "linux-64", + "mpi": "mpi", + } + ) + + # inject rattler-build recipe filters in jinja environment + env.filters.update( + { + "version_to_buildstring": _version_to_build_string, + "split": _split, + "bool": _bool, + } + ) + return env + + +def load_recipe_context( + context: dict[str, str], jinja_env: jinja2.Environment +) -> dict[str, str]: + """ + Load all string values from the context dictionary as Jinja2 templates. + Use linux-64 as default target_platform, build_platform, and mpi. + """ + # Process each key-value pair in the dictionary + for key, value in context.items(): + # If the value is a string, render it as a template + if isinstance(value, str): + template = jinja_env.from_string(value) + rendered_value = template.render(context) + context[key] = rendered_value + + return context diff --git a/conda_forge_tick/update_recipe/v2/jinja/objects.py b/conda_forge_tick/update_recipe/v2/jinja/objects.py new file mode 100644 index 000000000..32e64bab6 --- /dev/null +++ b/conda_forge_tick/update_recipe/v2/jinja/objects.py @@ -0,0 +1,35 @@ +from __future__ import annotations + + +class _StubEnv: + """A class to represent the env object used in rattler-build recipe.""" + + def get(self, env_var: str, default: str | None = None) -> str: # noqa: ARG002 + return f"""env_"{env_var}" """ + + def exists(self, env_var: str) -> str: + return f"""env_exists_"{env_var}" """ + + +def _stub_compatible_pin(*args, **kwargs) -> str: # noqa: ARG001, ANN003, ANN002 + return f"compatible_pin {args[0]}" + + +def _stub_subpackage_pin(*args, **kwargs) -> str: # noqa: ARG001, ANN003, ANN002 + return f"subpackage_pin {args[0]}" + + +def _stub_match(*args, **kwargs) -> str: # noqa: ARG001, ANN003, ANN002 + return f"match {args[0]}" + + +def _stub_is_unix(platform: str) -> str: + return f"is_unix {platform}" + + +def _stub_is_win(platform: str) -> str: + return f"is_win {platform}" + + +def _stub_is_linux(platform: str) -> str: + return f"is_linux {platform}" diff --git a/conda_forge_tick/update_recipe/v2/jinja/utils.py b/conda_forge_tick/update_recipe/v2/jinja/utils.py new file mode 100644 index 000000000..dc688e937 --- /dev/null +++ b/conda_forge_tick/update_recipe/v2/jinja/utils.py @@ -0,0 +1,11 @@ +from jinja2 import DebugUndefined + + +class _MissingUndefined(DebugUndefined): + def __str__(self) -> str: + """ + By default, `DebugUndefined` return values in the form `{{ value }}`. + `rattler-build` has a different syntax, so we need to override this method, + and return the value in the form `${{ value }}`. + """ + return f"${super().__str__()}" diff --git a/conda_forge_tick/update_recipe/v2/source.py b/conda_forge_tick/update_recipe/v2/source.py new file mode 100644 index 000000000..7e6b5d66a --- /dev/null +++ b/conda_forge_tick/update_recipe/v2/source.py @@ -0,0 +1,50 @@ +import typing +from typing import Any, Iterator, List, Mapping, NotRequired, TypedDict, Union + +from conda_forge_tick.update_recipe.v2.conditional_list import ( + ConditionalList, + visit_conditional_list, +) + +OptionalUrlList = Union[str, List[str], None] + + +class Source(TypedDict): + url: NotRequired[str | list[str]] + sha256: NotRequired[str] + md5: NotRequired[str] + + +def get_all_sources(recipe: Mapping[Any, Any]) -> Iterator[Source]: + """ + Get all sources from the recipe. This can be from a list of sources, + a single source, or conditional and its branches. + + Arguments + --------- + * `recipe` - The recipe to inspect. This should be a yaml object. + + Returns + ------- + A list of source objects. + """ + sources = recipe.get("source", None) + sources = typing.cast(ConditionalList[Source], sources) + + # Try getting all url top-level sources + if sources is not None: + source_list = visit_conditional_list(sources, None) + yield from source_list + + outputs = recipe.get("outputs", None) + if outputs is None: + return + + outputs = visit_conditional_list(outputs, None) + for output in outputs: + sources = output.get("source", None) + sources = typing.cast(ConditionalList[Source], sources) + if sources is None: + continue + source_list = visit_conditional_list(sources, None) + yield from source_list diff --git a/conda_forge_tick/update_recipe/v2/version.py b/conda_forge_tick/update_recipe/v2/version.py new file mode 100644 index 000000000..76eb70e64 --- /dev/null +++ b/conda_forge_tick/update_recipe/v2/version.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +import copy +import logging +from typing import TYPE_CHECKING, Literal + +from conda_forge_tick.update_recipe.v2.context import load_recipe_context +from conda_forge_tick.update_recipe.v2.jinja import jinja_env +from conda_forge_tick.update_recipe.v2.source import Source, get_all_sources +from conda_forge_tick.update_recipe.v2.yaml import _dump_yaml_to_str, _load_yaml +from conda_forge_tick.update_recipe.version import _try_url_and_hash_it + +if TYPE_CHECKING: + from pathlib import Path + +logger = logging.getLogger(__name__) + +HashType = Literal["md5", "sha256"] + + +class CouldNotUpdateVersionError(Exception): + NO_CONTEXT = "Could not find context in recipe" + NO_VERSION = "Could not find version in recipe context" + + def __init__(self, message: str = "Could not update version") -> None: + self.message = message + super().__init__(self.message) + + +class HashError(Exception): + def __init__(self, url: str) -> None: + self.url = url + self.message = f"Could not hash {url}" + super().__init__(self.message) + + +class Hash: + def __init__(self, hash_type: HashType, hash_value: str) -> None: + self.hash_type = hash_type + self.hash_value = hash_value + + def __str__(self) -> str: + return f"{self.hash_type}: {self.hash_value}" + + +def update_hash(source: Source, url: str, hash_: Hash | None) -> None: + """ + Update the sha256 hash in the source dictionary. + + Arguments: + ---------- + * `source` - The source dictionary to update. + * `url` - The URL to download and hash (if no hash is provided). + * `hash_` - The hash to use. If not provided, the file will be downloaded and `sha256` hashed. + """ + hash_type: HashType = hash_.hash_type if hash_ is not None else "sha256" + # delete all old hashes that we are not updating + all_hash_types: set[HashType] = {"md5", "sha256"} + for key in all_hash_types - {hash_type}: + if key in source: + del source[key] + + if hash_ is not None: + source[hash_.hash_type] = hash_.hash_value + else: + # download and hash the file + logger.debug(f"Retrieving and hashing {url}") + new_hash = _try_url_and_hash_it(url, "sha256") + if new_hash is None: + logger.error(f"Could not hash {url}") + raise HashError(url) + source["sha256"] = new_hash + + +def update_version( + file: Path, new_version: str, hash_: Hash | None +) -> (str | None, set[str]): + """ + Update the version in the recipe file. + + Arguments: + ---------- + * `file` - The path to the recipe file. + * `new_version` - The new version to use. + * `hash_type` - The hash type to use. If not provided, the file will be downloaded and `sha256` hashed. + + Returns: + -------- + * The updated recipe string (or None if there was an error). + * A set of errors that occurred during the update. + """ + + data = _load_yaml(file) + + if "context" not in data: + return None, {CouldNotUpdateVersionError.NO_CONTEXT} + if "version" not in data["context"]: + return None, {CouldNotUpdateVersionError.NO_VERSION} + + old_context = copy.deepcopy(data["context"]) + data["context"]["version"] = new_version + + CRAN_MIRROR = "https://cran.r-project.org" + + # set up the jinja context + env = jinja_env() + context = copy.deepcopy(data.get("context", {})) + old_context_variables = load_recipe_context(old_context, env) + old_context_variables["cran_mirror"] = CRAN_MIRROR + + new_context_variables = load_recipe_context(context, env) + # for r-recipes we add the default `cran_mirror` variable + new_context_variables["cran_mirror"] = CRAN_MIRROR + + errors: set[str] = set() + for source in get_all_sources(data): + # render the whole URL and find the hash + if "url" not in source: + continue + + url = source["url"] + if isinstance(url, list): + url = url[0] + + template = env.from_string(url) + old_rendered_url = template.render(old_context_variables) + rendered_url = template.render(new_context_variables) + + # nothing to do + if old_rendered_url == rendered_url: + continue + + try: + update_hash(source, rendered_url, hash_) + except HashError: + errors.add(f"Could not hash {url}") + + if errors: + return None, errors + + return _dump_yaml_to_str(data), {} diff --git a/conda_forge_tick/update_recipe/v2/yaml.py b/conda_forge_tick/update_recipe/v2/yaml.py new file mode 100644 index 000000000..fd5990636 --- /dev/null +++ b/conda_forge_tick/update_recipe/v2/yaml.py @@ -0,0 +1,22 @@ +import io +from pathlib import Path + +from ruamel.yaml import YAML + +yaml = YAML() +yaml.preserve_quotes = True +yaml.width = 320 +yaml.indent(mapping=2, sequence=4, offset=2) + + +def _load_yaml(file: Path) -> dict: + """Load a YAML file.""" + with file.open("r") as f: + return yaml.load(f) + + +def _dump_yaml_to_str(data: dict) -> str: + """Dump a dictionary to a YAML string.""" + with io.StringIO() as f: + yaml.dump(data, f) + return f.getvalue() diff --git a/conda_forge_tick/update_recipe/version.py b/conda_forge_tick/update_recipe/version.py index b3d709521..c9855db32 100644 --- a/conda_forge_tick/update_recipe/version.py +++ b/conda_forge_tick/update_recipe/version.py @@ -5,7 +5,7 @@ import pprint import re import traceback -from typing import Any, MutableMapping +from typing import Any, MutableMapping, Optional, Set import jinja2 import jinja2.sandbox @@ -99,7 +99,7 @@ def _compile_all_selectors(cmeta: Any, src: str): return set(selectors) -def _try_url_and_hash_it(url: str, hash_type: str): +def _try_url_and_hash_it(url: str, hash_type: str) -> Optional[str]: logger.debug("downloading url: %s", url) try: @@ -379,7 +379,9 @@ def _try_to_update_version(cmeta: Any, src: str, hash_type: str): return updated_version, errors -def update_version(raw_meta_yaml, version, hash_type="sha256"): +def update_version( + raw_meta_yaml: str, version: str, hash_type: str = "sha256" +) -> (Optional[str], Set[str]): """Update the version in a recipe. Parameters @@ -395,7 +397,7 @@ def update_version(raw_meta_yaml, version, hash_type="sha256"): ------- updated_meta_yaml : str or None The updated meta.yaml. Will be None if there is an error. - errors : str of str + errors : set of str A set of strings giving any errors found when updating the version. The set will be empty if there were no errors. """ diff --git a/tests/recipe_v2/build_number/test_1/expected.yaml b/tests/recipe_v2/build_number/test_1/expected.yaml new file mode 100644 index 000000000..f22fd1978 --- /dev/null +++ b/tests/recipe_v2/build_number/test_1/expected.yaml @@ -0,0 +1,10 @@ +# set the build number to something +context: + build: 0 + +package: + name: recipe_1 + version: "0.1.0" + +build: + number: ${{ build }} diff --git a/tests/recipe_v2/build_number/test_1/recipe.yaml b/tests/recipe_v2/build_number/test_1/recipe.yaml new file mode 100644 index 000000000..5ea4da2af --- /dev/null +++ b/tests/recipe_v2/build_number/test_1/recipe.yaml @@ -0,0 +1,10 @@ +# set the build number to something +context: + build: 123 + +package: + name: recipe_1 + version: "0.1.0" + +build: + number: ${{ build }} diff --git a/tests/recipe_v2/build_number/test_2/expected.yaml b/tests/recipe_v2/build_number/test_2/expected.yaml new file mode 100644 index 000000000..3095c27dd --- /dev/null +++ b/tests/recipe_v2/build_number/test_2/expected.yaml @@ -0,0 +1,11 @@ +# set the build number to something +package: + name: recipe_1 + version: "0.1.0" + +# set the build number to something directly in the recipe text +build: + number: 0 + +source: + - url: foo diff --git a/tests/recipe_v2/build_number/test_2/recipe.yaml b/tests/recipe_v2/build_number/test_2/recipe.yaml new file mode 100644 index 000000000..d0906a9a5 --- /dev/null +++ b/tests/recipe_v2/build_number/test_2/recipe.yaml @@ -0,0 +1,11 @@ +# set the build number to something +package: + name: recipe_1 + version: "0.1.0" + +# set the build number to something directly in the recipe text +build: + number: 321 + +source: +- url: foo diff --git a/tests/recipe_v2/version/test_1/expected.yaml b/tests/recipe_v2/version/test_1/expected.yaml new file mode 100644 index 000000000..02ad23f80 --- /dev/null +++ b/tests/recipe_v2/version/test_1/expected.yaml @@ -0,0 +1,12 @@ +context: + name: xtensor + version: "0.25.0" + + +package: + name: ${{ name|lower }} + version: ${{ version }} + +source: + url: https://github.com/xtensor-stack/xtensor/archive/${{ version }}.tar.gz + sha256: 32d5d9fd23998c57e746c375a544edf544b74f0a18ad6bc3c38cbba968d5e6c7 diff --git a/tests/recipe_v2/version/test_1/recipe.yaml b/tests/recipe_v2/version/test_1/recipe.yaml new file mode 100644 index 000000000..ca0cf055a --- /dev/null +++ b/tests/recipe_v2/version/test_1/recipe.yaml @@ -0,0 +1,12 @@ +context: + name: xtensor + version: "0.23.5" + + +package: + name: ${{ name|lower }} + version: ${{ version }} + +source: + url: https://github.com/xtensor-stack/xtensor/archive/${{ version }}.tar.gz + sha256: 0811011e448628f0dfa6ebb5e3f76dc7bf6a15ee65ea9c5a277b12ea976d35bc diff --git a/tests/recipe_v2/version/test_2/expected.yaml b/tests/recipe_v2/version/test_2/expected.yaml new file mode 100644 index 000000000..e9a70d0f9 --- /dev/null +++ b/tests/recipe_v2/version/test_2/expected.yaml @@ -0,0 +1,15 @@ +context: + name: xtensor + version: "0.25.0" + + +package: + name: ${{ name|lower }} + version: ${{ version }} + +source: + # please update the version here. + - if: target_platform == linux-64 + then: + url: https://github.com/xtensor-stack/xtensor/archive/${{ version }}.tar.gz + sha256: 32d5d9fd23998c57e746c375a544edf544b74f0a18ad6bc3c38cbba968d5e6c7 diff --git a/tests/recipe_v2/version/test_2/recipe.yaml b/tests/recipe_v2/version/test_2/recipe.yaml new file mode 100644 index 000000000..6de44d524 --- /dev/null +++ b/tests/recipe_v2/version/test_2/recipe.yaml @@ -0,0 +1,15 @@ +context: + name: xtensor + version: "0.23.5" + + +package: + name: ${{ name|lower }} + version: ${{ version }} + +source: + # please update the version here. + - if: target_platform == linux-64 + then: + url: https://github.com/xtensor-stack/xtensor/archive/${{ version }}.tar.gz + sha256: 0811011e448628f0dfa6ebb5e3f76dc7bf6a15ee65ea9c5a277b12ea976d35bc diff --git a/tests/recipe_v2/version/test_3/expected.yaml b/tests/recipe_v2/version/test_3/expected.yaml new file mode 100644 index 000000000..d6820f4bf --- /dev/null +++ b/tests/recipe_v2/version/test_3/expected.yaml @@ -0,0 +1,11 @@ +context: + name: pytest-aio + version: 1.9.0 + +package: + name: ${{ name|lower }} + version: ${{ version }} + +source: + url: https://pypi.io/packages/source/${{ name[0] }}/${{ name }}/${{ name.replace('-', '_') }}-${{ version }}.tar.gz + sha256: aa72e6ca4672b7f5a08ce44e7c6254dca988d3d578bf0c9485a47c3bff393ac1 diff --git a/tests/recipe_v2/version/test_3/recipe.yaml b/tests/recipe_v2/version/test_3/recipe.yaml new file mode 100644 index 000000000..cad30b78f --- /dev/null +++ b/tests/recipe_v2/version/test_3/recipe.yaml @@ -0,0 +1,11 @@ +context: + name: pytest-aio + version: 1.8.1 + +package: + name: ${{ name|lower }} + version: ${{ version }} + +source: + url: https://pypi.io/packages/source/${{ name[0] }}/${{ name }}/${{ name.replace('-', '_') }}-${{ version }}.tar.gz + sha256: 97dcbc1c5ac991705f32bb2cf72f9ba94a8889fd0295d29ed4d7252b3e158684 diff --git a/tests/recipe_v2/version/test_4/expected.yaml b/tests/recipe_v2/version/test_4/expected.yaml new file mode 100644 index 000000000..2cb6a9cd4 --- /dev/null +++ b/tests/recipe_v2/version/test_4/expected.yaml @@ -0,0 +1,14 @@ +context: + version: "1.1-30" + posix: ${{ 'm2' if win else '' }} + native: ${{ 'm2w64' if win else '' }} + +package: + name: r-systemfit + version: ${{ version|replace("-", "_") }} + +source: + url: + - ${{ cran_mirror }}/src/contrib/systemfit_${{ version }}.tar.gz + - ${{ cran_mirror }}/src/contrib/Archive/systemfit/systemfit_${{ version }}.tar.gz + sha256: 5994fbb81f1678325862414f58328cdc2c46d47efa1f23218e9416a4da431ce2 diff --git a/tests/recipe_v2/version/test_4/recipe.yaml b/tests/recipe_v2/version/test_4/recipe.yaml new file mode 100644 index 000000000..fb35a82f5 --- /dev/null +++ b/tests/recipe_v2/version/test_4/recipe.yaml @@ -0,0 +1,14 @@ +context: + version: "1.1-26" + posix: ${{ 'm2' if win else '' }} + native: ${{ 'm2w64' if win else '' }} + +package: + name: r-systemfit + version: ${{ version|replace("-", "_") }} + +source: + url: + - ${{ cran_mirror }}/src/contrib/systemfit_${{ version }}.tar.gz + - ${{ cran_mirror }}/src/contrib/Archive/systemfit/systemfit_${{ version }}.tar.gz + sha256: a99a59787dc5556afe9a1a153f2a3a8047aa7d357aab450101e20ab1f329f758 diff --git a/tests/test_recipe_editing_v2.py b/tests/test_recipe_editing_v2.py new file mode 100644 index 000000000..8adacdd92 --- /dev/null +++ b/tests/test_recipe_editing_v2.py @@ -0,0 +1,40 @@ +from pathlib import Path + +import pytest + +from conda_forge_tick.update_recipe.v2 import update_build_number, update_version + + +@pytest.fixture +def data_dir() -> Path: + return Path(__file__).parent / "recipe_v2" + + +def test_build_number_mod(data_dir: Path) -> None: + tests = data_dir / "build_number" + result = update_build_number(tests / "test_1/recipe.yaml", 0) + expected = tests / "test_1/expected.yaml" + assert result == expected.read_text() + + result = update_build_number(tests / "test_2/recipe.yaml", 0) + expected = tests / "test_2/expected.yaml" + assert result == expected.read_text() + + +def test_version_mod(data_dir: Path) -> None: + tests = data_dir / "version" + test_recipes = [tests / "test_1/recipe.yaml", tests / "test_2/recipe.yaml"] + for recipe in test_recipes: + result = update_version(recipe, "0.25.0", None)[0] + expected = recipe.parent / "expected.yaml" + assert result == expected.read_text() + + test_python = tests / "test_3/recipe.yaml" + result = update_version(test_python, "1.9.0", None)[0] + expected = test_python.parent / "expected.yaml" + assert result == expected.read_text() + + test_cran = tests / "test_4/recipe.yaml" + result = update_version(test_cran, "1.1-30", None)[0] + expected = test_cran.parent / "expected.yaml" + assert result == expected.read_text()