Source code for conda_recipe_manager.parser.variants_manager
"""
:Description: Provides a class that manages the variants of a recipe, given a list of CBC files.
"""
from __future__ import annotations
import json
from typing import Final, cast
from conda_recipe_manager.parser.build_context import BuildContext
from conda_recipe_manager.parser.cbc_reader import CbcReader
from conda_recipe_manager.parser.recipe_reader_deps import RecipeReaderDeps
from conda_recipe_manager.parser.recipe_variant import RecipeVariant
from conda_recipe_manager.parser.types import GeneratedVariantsType, NoArchType, RecipeReaderFlags
from conda_recipe_manager.types import PRIMITIVES_TUPLE, Primitives
[docs]
class VariantsManager:
"""
Class that manages the variants of a recipe, given a list of CBC files.
"""
def __init__(
self,
recipe_str: str,
cbc_strs: list[str],
build_context: BuildContext,
flags: RecipeReaderFlags = RecipeReaderFlags.NONE,
):
"""
Initializes the VariantsManager.
:param recipe_str: String representation of the recipe.
:param cbc_strs: List of string representations of the CBC files.
:param build_context: Build context to generate the variants for.
:param flags: RecipeReaderFlags to be set. Defaults to `RecipeReaderFlags.NONE`.
"""
self._build_context = build_context
self._cbc_parsers: list[CbcReader] = [CbcReader(cbc_str) for cbc_str in cbc_strs]
variants: Final[GeneratedVariantsType] = CbcReader.generate_variants(self._cbc_parsers, build_context)
self._base_recipe: RecipeReaderDeps = RecipeReaderDeps(recipe_str, flags=flags)
self._recipe_variants: list[RecipeVariant] = []
# Tracks the structure of the recipe variant AND variable usage. Selector evaluations can cause changes that may
# be opaque when comparing variable usage exclusively. Conversely, identically-structured recipes may vary
# if they use variables with multiple defined values.
known_used_vars_by_hash: dict[str, set[str]] = {}
for full_var in variants:
variant = {key: value for key, value in full_var.items() if isinstance(value, PRIMITIVES_TUPLE)}
post_cbc_build_context: BuildContext = BuildContext(
build_context.get_platform(), {**build_context.get_context(), **variant}
)
recipe_variant = RecipeVariant(recipe_str, post_cbc_build_context, flags=flags)
if recipe_variant.get_value("/build/skip", default=None, sub_vars=True) is True:
continue
# De-duplicate identical variations while also mimicking conda-build's behavior around `python` versions.
# We do this by checking variable usage within rendered variations.
# NOTE:
# - Selectors should be fully evaluated by the `RecipeVariant` class at construction, so we don't need to
# worry about CBC variables used in selectors. BUT we DO have to worry about selectors changing the
# structure of the variants.
# - We have to assume that variable usage changes with selector evaluations, so we can't save compute by
# "caching" the variable values seen in the unrendered recipe file.
recipe_variant_vars = recipe_variant.list_variables()
# The `python` variable found commonly in CBC files is treated differently. Recipe files rarely, if ever,
# reference `python` as a selector or variable. Yet the expected behavior in `conda-build` is to generate
# 1-variant-per-python version.
# TODO Future: Figure out if there are other common CBC variables that work this way.
python_version = None if not recipe_variant.is_python_recipe() else variant.get("python", None)
used_vars: dict[str, Primitives] = {
var: cast(Primitives, recipe_variant.get_variable(var)) for var in recipe_variant_vars if var in variant
}
if python_version is not None:
# `noarch` Python packages should only have 1-Python variant, so we use the same value for all `python`
# versions.
noarch_type = recipe_variant.get_noarch_type()
used_vars["python"] = str(noarch_type) if noarch_type == NoArchType.PYTHON else python_version
serialized_used_vars = json.dumps(used_vars, sort_keys=True)
recipe_variant_hash: str = recipe_variant.calc_sha256()
if recipe_variant_hash in known_used_vars_by_hash:
if serialized_used_vars in known_used_vars_by_hash[recipe_variant_hash]:
continue
else:
# Allocate set on first hash
known_used_vars_by_hash[recipe_variant_hash] = set()
known_used_vars_by_hash[recipe_variant_hash].add(serialized_used_vars)
self._recipe_variants.append(recipe_variant)
[docs]
def get_base_recipe(self) -> RecipeReaderDeps:
"""
Returns the base (unrendered) recipe instance.
:returns: The base recipe instance.
"""
return self._base_recipe
[docs]
def get_recipe_variants(self) -> list[RecipeVariant]:
"""
Returns the recipe variants as a list.
:returns: The rendered recipe variants, as a list.
"""
return self._recipe_variants
[docs]
def get_cbc_parsers(self) -> list[CbcReader]:
"""
Returns the Conda Build Config parsers.
:returns: A list of Conda Build Config (CBC) reader-instances that initialized this instance.
"""
return self._cbc_parsers