Source code for conda_recipe_manager.parser.recipe_reader_deps

"""
:Description: Provides a subclass of RecipeReader that adds advanced dependency management tools.
"""

from __future__ import annotations

from typing import Final, Optional, cast

from conda_recipe_manager.parser._types import ROOT_NODE_VALUE
from conda_recipe_manager.parser.dependency import (
    Dependency,
    DependencyMap,
    dependency_data_from_str,
    str_to_dependency_section,
)
from conda_recipe_manager.parser.recipe_reader import RecipeReader
from conda_recipe_manager.parser.selector_parser import SelectorParser
from conda_recipe_manager.parser.types import SchemaVersion


[docs] class RecipeReaderDeps(RecipeReader): """ Extension of the base RecipeReader class to enables advanced dependency management abilities. The base RecipeReader class is so large, that this has been broken-out for maintenance purposes. """ @staticmethod def _add_top_level_dependencies(root_package: str, dep_map: DependencyMap) -> None: """ Helper function that applies "root"/top-level dependencies to packages in multi-output recipes. """ if len(dep_map) <= 1 or root_package not in dep_map: return root_dependencies: Final[list[Dependency]] = dep_map[root_package] for package in dep_map: if package == root_package: continue # Change the "required_by" package name to the current package, not the root package name. dep_map[package].extend( [Dependency(package, d.path, d.type, d.data, d.selector) for d in root_dependencies] ) @staticmethod def _sanitize_dep(dep: Optional[str]) -> Optional[str]: """ Sanitizes dependency strings. Invalid dependencies can be ignored with a `None` check. This function prevents consumption of bad recipe file data. :param dep: Dependency string to validate. This is the string found in a list in a dependency section. :returns: The sanitized string, if valid. `None`, if invalid. """ if dep is None: return None dep = dep.strip() if not dep: return None return dep def _fetch_optional_selector(self, path: str) -> Optional[SelectorParser]: """ Given a recipe path, optionally return a SelectorParser object. :param path: Path to the target value :returns: A parsed selector, if one is available. Otherwise, None. """ try: return SelectorParser(self.get_selector_at_path(path), self._schema_version) except KeyError: return None
[docs] def get_package_names_to_path(self) -> dict[str, str]: """ Get a map containing all the packages (artifacts) named in a recipe to their paths in the recipe structure. :raises KeyError: If a package in the recipe does not have a name :raises ValueError: If a recipe contains a package with duplicate names :returns: Mapping of package name to path where that package is found """ # TODO Figure out: Skip top-level packages for multi-output recipe files? package_tbl: dict[str, str] = {} root_name_path: Final[str] = ( "/recipe/name" if self.is_multi_output() and self._schema_version == SchemaVersion.V1 else "/package/name" ) name_path: Final[str] = ( "/package/name" if self.is_multi_output() and self._schema_version == SchemaVersion.V1 else "/name" ) for path in self.get_package_paths(): try: if path == ROOT_NODE_VALUE: name = cast(str, self.get_value(root_name_path, sub_vars=True)) else: name = cast(str, self.get_value(RecipeReader.append_to_path(path, name_path), sub_vars=True)) except KeyError as e: raise KeyError(f"Could not find a package name associated with path: {path}") from e if name in package_tbl: raise ValueError(f"Duplicate package name found: {name}") package_tbl[name] = path return package_tbl
[docs] def get_all_dependencies(self) -> DependencyMap: """ Get a parsed representation of all the dependencies found in the recipe. :raises KeyError: If a package in the recipe does not have a name :raises ValueError: If a recipe contains a package with duplicate names :returns: A structured representation of the dependencies. """ # TODO Figure out: Skip top-level packages for multi-output recipe files? package_path_tbl: Final[dict[str, str]] = self.get_package_names_to_path() root_package = "" dep_map: DependencyMap = {} for package, path in package_path_tbl.items(): if path == ROOT_NODE_VALUE: root_package = package requirements = cast( dict[str, list[Optional[str]]], self.get_value(RecipeReader.append_to_path(path, "/requirements"), default={}, sub_vars=True), ) dep_map[package] = [] for section_str, deps in requirements.items(): section = str_to_dependency_section(section_str) # Unrecognized sections will be skipped as "junk" data if section is None: continue for i, dep in enumerate(deps): dep = RecipeReaderDeps._sanitize_dep(dep) if dep is None: continue # NOTE: `get_dependency_paths()` uses the same approach for calculating dependency paths. dep_path = RecipeReader.append_to_path(path, f"/requirements/{section_str}/{i}") dep_map[package].append( Dependency( required_by=package, path=dep_path, type=section, data=dependency_data_from_str(dep), selector=self._fetch_optional_selector(dep_path), ) ) # Apply top-level dependencies to multi-output recipe packages RecipeReaderDeps._add_top_level_dependencies(root_package, dep_map) return dep_map