Source code for conda_recipe_manager.parser.dependency

"""
:Description: Provides types and utilities for managing recipe dependencies.
"""

from __future__ import annotations

from enum import Enum, auto
from typing import NamedTuple, Optional, cast

from conda.models.match_spec import InvalidMatchSpec, MatchSpec

from conda_recipe_manager.parser._types import Regex
from conda_recipe_manager.parser.selector_parser import SelectorParser
from conda_recipe_manager.parser.types import SchemaVersion


[docs] class DependencySection(Enum): """ Enumerates dependency sections found in a recipe file. """ BUILD = auto() HOST = auto() RUN = auto() RUN_CONSTRAINTS = auto() # NOTE: `run_exports` was not in the `requirements/` section in the V0 format RUN_EXPORTS = auto() # NOTE: # - Test dependencies are not found under the `requirements/` section, they are found under the testing section. # - There are major changes to the testing section in V1. # TODO TEST not covered in get_all_dependencies() TESTS = auto()
[docs] class DependencyConflictMode(Enum): """ Mode of operation to use when handling duplicate dependencies (identified by name). """ # Replace the existing dependency with the incoming dependency. Append to the end if there is no duplicate. REPLACE = auto() # Ignore the incoming dependency if there is a duplicate and do not modify any existing selector. Otherwise, append # to the end of the list. IGNORE = auto() # Include both dependencies, always appending to the end of the list. USE_BOTH = auto() # Write over what exists at the index provided, regardless of duplicates. EXACT_POSITION = auto()
[docs] def dependency_section_to_str(section: DependencySection, schema: SchemaVersion) -> str: """ Converts a dependency section enumeration to the equivalent string found in the recipe, based on the current schema. :param section: Target dependency section :param schema: Target recipe schema :returns: String equivalent of the recipe schema """ # `match` is used here so the static analyzer can ensure all cases are covered match schema: case SchemaVersion.V0: match section: case DependencySection.BUILD: return "build" case DependencySection.HOST: return "host" case DependencySection.RUN: return "run" case DependencySection.RUN_CONSTRAINTS: return "run_constrained" case DependencySection.RUN_EXPORTS: return "run_exports" case DependencySection.TESTS: return "requires" case SchemaVersion.V1: match section: case DependencySection.BUILD: return "build" case DependencySection.HOST: return "host" case DependencySection.RUN: return "run" case DependencySection.RUN_CONSTRAINTS: return "run_constraints" case DependencySection.RUN_EXPORTS: return "run_exports" case DependencySection.TESTS: return "requires"
[docs] def str_to_dependency_section(s: str) -> Optional[DependencySection]: """ Converts a dependency section string to a section enumeration. :param s: Target string to convert :returns: String equivalent of the recipe schema. None if the string is unrecognized. """ # `match` is used here so the static analyzer can ensure all cases are covered match s.strip().lower(): case "build": return DependencySection.BUILD case "host": return DependencySection.HOST case "run": return DependencySection.RUN case "run_constrained": # V0 return DependencySection.RUN_CONSTRAINTS case "run_constraints": # V1 return DependencySection.RUN_CONSTRAINTS case "run_exports": return DependencySection.RUN_EXPORTS # This is included for the sake of completeness. Realistically, test dependencies should be detected by looking # at the testing section, not `/requirements`. case "requires": return DependencySection.TESTS case _: return None
[docs] class DependencyVariable: """ Represents a dependency that contains a JINJA variable that is unable to be resolved by the recipe's variable table. """ def __init__(self, s: str): """ Constructs a DependencyVariable instance. :param s: String to initialize the instance with. """ # Using `name` allows this class to be used trivially with MatchSpec without type guards. # TODO normalize common JINJA functions for quote usage self.name = s def __eq__(self, o: object) -> bool: """ Checks to see if two objects are equivalent. :param o: Other instance to check. :returns: True if two DependencyVariable instances are equivalent. False otherwise. """ if not isinstance(o, DependencyVariable): return False return self.name == o.name def __hash__(self) -> int: """ Hashes this `DependencyVariable` instance. :returns: The hash value of this object. """ return hash(self.name)
# Type alias for types allowed in a Dependency's `data` field. DependencyData = MatchSpec | DependencyVariable
[docs] def dependency_data_from_str(s: str) -> DependencyData: """ Constructs a `DependencyData` object from a dependency string in a recipe file. :param s: String to process. :returns: A `DependencyData` instance. """ if Regex.JINJA_V0_SUB.search(s) or Regex.JINJA_V1_SUB.search(s): return DependencyVariable(s) try: return MatchSpec(s) except (ValueError, InvalidMatchSpec): # In an effort to be more resilient, fallback to the simpler type. return DependencyVariable(s)
[docs] def dependency_data_render_as_str(data: DependencyData) -> str: """ Given a `DependencyData` instance, derive the original string found in the recipe. :param data: Target `DependencyData` :return s: The original (raw) string found in the recipe file. """ match data: case MatchSpec(): return cast(str, data.original_spec_str) case DependencyVariable(): return data.name
[docs] class Dependency(NamedTuple): """ Structure that contains metadata about a dependency found in the recipe. This is immutable by design. """ # Owning package name required_by: str # Path in the recipe where this dependency was found path: str # Identifies what kind of dependency this is type: DependencySection # Parsed dependency. Identifies a name and version constraints data: DependencyData # The selector applied to this dependency, if applicable selector: Optional[SelectorParser] = None
# Maps-out dependencies found in a recipe. Maps package name -> list of parsed dependencies. DependencyMap = dict[str, list[Dependency]]