Source code for conda_recipe_manager.parser.cbc_parser

"""
:Description: Parser that is capable of comprehending Conda Build Configuration (CBC) files.
"""

from __future__ import annotations

from typing import Final, NamedTuple, Optional, cast

from conda_recipe_manager.parser.recipe_reader import RecipeReader
from conda_recipe_manager.parser.selector_parser import SelectorParser
from conda_recipe_manager.parser.selector_query import SelectorQuery
from conda_recipe_manager.parser.types import SchemaVersion
from conda_recipe_manager.types import Primitives, SentinelType


class _CBCEntry(NamedTuple):
    """
    Internal representation of a variable's value in a CBC file.
    """

    value: Primitives
    selector: Optional[SelectorParser]


# Internal variable table type
_CbcTable = dict[str, list[_CBCEntry]]

# Type that attempts to represent the contents of a CBC file
_CbcType = dict[str, list[Primitives] | dict[str, dict[str, str]]]


[docs] class CbcParser(RecipeReader): """ Parses a Conda Build Configuration (CBC) file and provides querying capabilities. Often these files are named `conda_build_configuration.yaml` or `cbc.yaml` This work is based off of the `RecipeReader` class. The CBC file format happens to be similar enough to the recipe format (with commented selectors) """ # TODO: Add V1-support for the new CBC equivalent: # https://prefix-dev.github.io/rattler-build/latest/variants/ def __init__(self, content: str): """ Constructs a CBC Parser instance from the contents of a CBC file. :param content: conda-build formatted configuration file, as a single text string. """ super().__init__(content) self._cbc_vars_tbl: _CbcTable = {} # TODO Handle special cases: # - pin_run_as_build # - zip_keys # - python (versions) # - numpy # - The CBC file matches the python version and numpy version by list index # - r_implementation # From Charles: "Compared to meta.yaml, no jinja is allowed in the cbc. Also I believe only the base subset of # selectors is available (so py>=38 and py<=310 wouldn't work). To be confirmed though." parsed_contents: Final[_CbcType] = cast(_CbcType, self.get_value("/")) for variable, value_list in parsed_contents.items(): if not isinstance(value_list, list): continue # TODO track and calculate zip-key values if variable == "zip_keys": continue for i, value in enumerate(value_list): path = f"/{variable}/{i}" # TODO add V1 support for CBC files? Is there a V1 CBC format? selector = None try: selector = SelectorParser(self.get_selector_at_path(path), SchemaVersion.V0) except KeyError: pass entry = _CBCEntry( value=value, selector=selector, ) # TODO detect duplicates if variable not in self._cbc_vars_tbl: self._cbc_vars_tbl[variable] = [entry] else: self._cbc_vars_tbl[variable].append(entry) def __contains__(self, key: object) -> bool: """ Indicates if a variable is found in a CBC file. :param key: Target variable name to check for. :returns: True if the variable exists in this CBC file. False otherwise. """ if not isinstance(key, str): return False return key in self._cbc_vars_tbl
[docs] def list_cbc_variables(self) -> list[str]: """ Get a list of all the available CBC variable names. :returns: A list containing all the variables defined in the CBC file. """ return list(self._cbc_vars_tbl.keys())
[docs] def get_cbc_variable_value( self, variable: str, query: SelectorQuery, default: Primitives | SentinelType = RecipeReader._sentinel ) -> Primitives: """ Determines which value of a CBC variable is applicable to the current environment. :param variable: Target variable name. :param query: Query that represents the state of the target build environment. :param default: (Optional) Value to return if no variable could be found or no value could be determined. :raises KeyError: If the key does not exist and no default value is provided. :raises ValueError: If the selector query does not match any case and no default value is provided. :returns: Value of the variable as indicated by the selector options provided. """ if variable not in self: if isinstance(default, SentinelType): raise KeyError(f"CBC variable not found: {variable}") return default cbc_entries: Final[list[_CBCEntry]] = self._cbc_vars_tbl[variable] # Short-circuit on trivial case: one value, no selector if len(cbc_entries) == 1 and cbc_entries[0].selector is None: return cbc_entries[0].value for entry in cbc_entries: if entry.selector is None or entry.selector.does_selector_apply(query): return entry.value # No applicable entries have been found to match any selector variant. if isinstance(default, SentinelType): raise ValueError(f"CBC variable does not have a value for the provided selector query: {variable}") return default