diff --git a/src/rattler_build_conda_compat/render.py b/src/rattler_build_conda_compat/render.py index bf0aeed..f58aac0 100644 --- a/src/rattler_build_conda_compat/render.py +++ b/src/rattler_build_conda_compat/render.py @@ -1,11 +1,15 @@ # mypy: ignore-errors +from __future__ import annotations + from collections import OrderedDict import json import os +from pathlib import Path import subprocess +import sys import tempfile -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional import yaml from ruamel.yaml import YAML from conda_build.metadata import ( @@ -19,11 +23,12 @@ validate_spec, combine_specs, ) -from conda_build.metadata import get_selectors +from conda_build.metadata import get_selectors, check_bad_chrs from conda_build.config import Config -from rattler_build_conda_compat.loader import parse_recipe_config_file -from rattler_build_conda_compat.utils import find_recipe +from rattler_build_conda_compat.jinja.jinja import render_recipe_with_context +from rattler_build_conda_compat.loader import load_yaml, parse_recipe_config_file +from rattler_build_conda_compat.utils import _get_recipe_metadata, find_recipe class MetaData(CondaMetaData): @@ -59,10 +64,56 @@ def __init__( self.requirements_path = os.path.join(self.path, "requirements.txt") - def parse_recipe(self): - yaml = YAML() - with open(os.path.join(self.path, self._meta_name), "r") as recipe_yaml: - return yaml.load(recipe_yaml) + def parse_recipe(self) -> dict[str, Any]: + recipe_path: Path = Path(self.path) / self._meta_name + + yaml_content = load_yaml(recipe_path.read_text()) + + return render_recipe_with_context(yaml_content) + + def name(self) -> str: + """ + Overrides the conda_build.metadata.MetaData.name method. + Returns the name of the package. + If recipe has multiple outputs, it will return the name of the `recipe` field. + Otherwise it will return the name of the `package` field. + + Raises: + - CondaBuildUserError: If the `name` contains bad characters. + - ValueError: If the name is not lowercase or missing. + + """ + name = _get_recipe_metadata(self.meta, "name") + + if not name: + raise ValueError(f"Error: package/name missing in: {self.meta_path!r}") + + if name != name.lower(): + raise ValueError(f"Error: package/name must be lowercase, got: {name!r}") + + check_bad_chrs(name, "package/name") + return name + + def version(self) -> str: + """ + Overrides the conda_build.metadata.MetaData.version method. + Returns the version of the package. + If recipe has multiple outputs, it will return the version of the `recipe` field. + Otherwise it will return the version of the `package` field. + + Raises: + - CondaBuildUserError: If the `version` contains bad characters. + - ValueError: If the version starts with a period or version is missing. + """ + version = _get_recipe_metadata(self.meta, "version") + + if not version: + raise ValueError(f"Error: package/version missing in: {self.meta_path!r}") + + check_bad_chrs(version, "package/version") + if version.startswith("."): + raise ValueError(f"Fully-rendered version can't start with period - got {version!r}") + return version def render_recipes(self, variants) -> List[Dict]: platform_and_arch = f"{self.config.platform}-{self.config.arch}" diff --git a/src/rattler_build_conda_compat/utils.py b/src/rattler_build_conda_compat/utils.py index 202b15a..97f3170 100644 --- a/src/rattler_build_conda_compat/utils.py +++ b/src/rattler_build_conda_compat/utils.py @@ -1,8 +1,9 @@ +from __future__ import annotations import fnmatch from logging import getLogger import os from pathlib import Path -from typing import Iterable +from typing import Any, Iterable, Literal VALID_METAS = ("recipe.yaml",) @@ -171,3 +172,17 @@ def has_recipe(recipe_dir: Path) -> bool: return False except OSError: return False + + +_Metadata = Literal["name", "version"] + + +def _get_recipe_metadata(meta: dict[str, Any], field: _Metadata) -> str: + """ + Get recipe metadata ( name or version ). + It will extract from recipe or package section, depending on the presence of multiple outputs. + """ + if "outputs" in meta: + return meta.get("recipe", {}).get(field, "") + else: + return meta.get("package", {}).get(field, "") diff --git a/tests/__snapshots__/test_jinja.ambr b/tests/__snapshots__/test_jinja.ambr index 18d5008..688be6b 100644 --- a/tests/__snapshots__/test_jinja.ambr +++ b/tests/__snapshots__/test_jinja.ambr @@ -137,6 +137,7 @@ - test -f ${PREFIX}/condabin/mamba recipe: name: mamba-split + version: 1.5.8 source: sha256: 6ddaf4b0758eb7ca1250f427bc40c2c3ede43257a60bac54e4320a4de66759a6 url: https://github.com/mamba-org/mamba/archive/refs/tags/2024.03.25.tar.gz diff --git a/tests/__snapshots__/test_rattler_render.ambr b/tests/__snapshots__/test_rattler_render.ambr index 099b9c1..2fad261 100644 --- a/tests/__snapshots__/test_rattler_render.ambr +++ b/tests/__snapshots__/test_rattler_render.ambr @@ -1,33 +1,33 @@ # serializer version: 1 # name: test_environ_is_passed_to_rattler_build list([ - CommentedMap({ - 'package': CommentedMap({ - 'name': 'py-test', - 'version': '1.0.0', + dict({ + 'about': dict({ }), - 'build': CommentedMap({ - 'skip': CommentedSeq([ + 'build': dict({ + 'skip': list([ "env.get('TEST_SHOULD_BE_PASSED') == 'false'", ]), }), - 'requirements': CommentedMap({ - 'build': CommentedSeq([ - "${{ compiler('c') }}", - "${{ compiler('cuda') }}", + 'extra': dict({ + 'final': True, + }), + 'package': dict({ + 'name': 'py-test', + 'version': '1.0.0', + }), + 'requirements': dict({ + 'build': list([ + 'c_compiler_stub', + 'cuda_compiler_stub', ]), - 'host': CommentedSeq([ + 'host': list([ 'python', ]), - 'run': CommentedSeq([ + 'run': list([ 'python', ]), }), - 'about': dict({ - }), - 'extra': dict({ - 'final': True, - }), }), ]) # --- diff --git a/tests/conftest.py b/tests/conftest.py index 57b908e..7873347 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -60,3 +60,25 @@ def old_recipe_dir(tmpdir: Path) -> Path: meta.touch() return recipe_dir + + +@pytest.fixture() +def mamba_recipe() -> Path: + return Path("tests/data/mamba_recipe.yaml") + + +@pytest.fixture() +def rich_recipe() -> Path: + return Path("tests/data/rich_recipe.yaml") + + +@pytest.fixture() +def feedstock_dir_with_recipe(tmpdir: Path) -> Path: + feedstock_dir = tmpdir / "feedstock" + + feedstock_dir.mkdir() + + recipe_dir = feedstock_dir / "recipe" + recipe_dir.mkdir() + + return feedstock_dir diff --git a/tests/data/mamba_recipe.yaml b/tests/data/mamba_recipe.yaml index 0478704..27af2a9 100644 --- a/tests/data/mamba_recipe.yaml +++ b/tests/data/mamba_recipe.yaml @@ -10,6 +10,7 @@ context: recipe: name: mamba-split + version: ${{ mamba_version }} source: url: https://github.com/mamba-org/mamba/archive/refs/tags/${{ release }}.tar.gz diff --git a/tests/data/rich_recipe.yaml b/tests/data/rich_recipe.yaml new file mode 100644 index 0000000..a02c1d0 --- /dev/null +++ b/tests/data/rich_recipe.yaml @@ -0,0 +1,55 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json + +context: + version: "13.4.2" + name: rich + +package: + name: ${{ name}} + version: ${{ version }} + +source: + - url: + - https://example.com/rich-${{ version }}.tar.gz # this will give a 404! + - https://pypi.io/packages/source/r/rich/rich-${{ version }}.tar.gz + sha256: d653d6bccede5844304c605d5aac802c7cf9621efd700b46c7ec2b51ea914898 + +build: + # Thanks to `noarch: python` this package works on all platforms + noarch: python + script: + - python -m pip install . -vv --no-deps --no-build-isolation + +requirements: + host: + - pip + - poetry-core >=1.0.0 + - python 3.10 + run: + # sync with normalized deps from poetry-generated setup.py + - markdown-it-py >=2.2.0 + - pygments >=2.13.0,<3.0.0 + - python 3.10 + - typing_extensions >=4.0.0,<5.0.0 + +tests: + - package_contents: + site_packages: + - rich + - python: + imports: + - rich + +about: + homepage: https://github.com/Textualize/rich + license: MIT + license_file: LICENSE + summary: Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal + description: | + Rich is a Python library for rich text and beautiful formatting in the terminal. + + The Rich API makes it easy to add color and style to terminal output. Rich + can also render pretty tables, progress bars, markdown, syntax highlighted + source code, tracebacks, and more — out of the box. + documentation: https://rich.readthedocs.io + repository: https://github.com/Textualize/rich diff --git a/tests/test_rattler_render.py b/tests/test_rattler_render.py index d384d3b..7c130d2 100644 --- a/tests/test_rattler_render.py +++ b/tests/test_rattler_render.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any from rattler_build_conda_compat.loader import parse_recipe_config_file -from rattler_build_conda_compat.render import render +from rattler_build_conda_compat.render import MetaData, render if TYPE_CHECKING: from pathlib import Path @@ -43,3 +43,25 @@ def test_environ_is_passed_to_rattler_build(env_recipe, snapshot) -> None: finally: os.environ.pop("TEST_SHOULD_BE_PASSED", None) + + +def test_metadata_for_single_output(feedstock_dir_with_recipe: Path, rich_recipe: Path) -> None: + (feedstock_dir_with_recipe / "recipe" / "recipe.yaml").write_text( + rich_recipe.read_text(), encoding="utf8" + ) + + rattler_metadata = MetaData(feedstock_dir_with_recipe) + + assert rattler_metadata.name() == "rich" + assert rattler_metadata.version() == "13.4.2" + + +def test_metadata_for_multiple_output(feedstock_dir_with_recipe: Path, mamba_recipe: Path) -> None: + (feedstock_dir_with_recipe / "recipe" / "recipe.yaml").write_text( + mamba_recipe.read_text(), encoding="utf8" + ) + + rattler_metadata = MetaData(feedstock_dir_with_recipe) + + assert rattler_metadata.name() == "mamba-split" + assert rattler_metadata.version() == "1.5.8"