Source code for conda_recipe_manager.scanner.dependency.pyproject_dep_scanner

"""
:Description: Reads dependencies from a `pyproject.toml` file.
"""

from __future__ import annotations

import tomllib
from pathlib import Path
from typing import Final, cast

from conda_recipe_manager.parser.dependency import DependencySection
from conda_recipe_manager.scanner.dependency.base_dep_scanner import (
    BaseDependencyScanner,
    ProjectDependency,
    new_project_dependency,
)
from conda_recipe_manager.types import MessageCategory


[docs] class PyProjectDependencyScanner(BaseDependencyScanner): """ Dependency Scanner class capable of scanning `pyproject.toml` files. """ def __init__(self, src_dir: Path | str, project_file_name: str = "pyproject.toml"): """ Constructs a `PyProjectDependencyScanner`. :param src_dir: Path to the Python source code to scan. :param project_file_name: (Optional) Allows for custom pyproject file names. Primarily used for testing, defaults to standard `pyproject.toml` name. """ super().__init__() self._src_dir: Final[Path] = Path(src_dir) self._project_fn: Final[str] = project_file_name
[docs] def scan(self) -> set[ProjectDependency]: """ Actively scans a project for dependencies. Implementation is dependent on the type of scanner used. :returns: A set of unique dependencies found by the scanner, if any are found. """ try: with open(self._src_dir / self._project_fn, "rb") as f: data = cast(dict[str, dict[str, list[str] | dict[str, list[str]]]], tomllib.load(f)) except (FileNotFoundError, tomllib.TOMLDecodeError) as e: if isinstance(e, FileNotFoundError): self._msg_tbl.add_message(MessageCategory.EXCEPTION, f"`{self._project_fn}` file not found.") if isinstance(e, tomllib.TOMLDecodeError): self._msg_tbl.add_message(MessageCategory.EXCEPTION, f"Could not parse `{self._project_fn}` file.") return set() # NOTE: There is a `validate-pyproject` library hosted on `conda-forge`, but it is marked as "experimental" by # its maintainers. Given that and that we only read a small portion of the file, we only validate what we use. if "project" not in data: self._msg_tbl.add_message( MessageCategory.ERROR, f"`{self._project_fn}` file is missing a `project` section." ) return set() # NOTE: The dependency constraint system used in `pyproject.toml` appears to be compatible with `conda`'s # `MatchSpec` object. For now, dependencies that can't be parsed with `MatchSpec` will store the raw string in # a `.name` field. # TODO Future, consider handling Environment Markers: # https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers deps: set[ProjectDependency] = set() for dep_name in cast(list[str], data["project"].get("dependencies", [])): deps.add(new_project_dependency(dep_name, DependencySection.RUN)) # Optional dependencies are stored in a dictionary, where the key is the "package extra" name and the value is # a dependency list. For example: {'dev': ['pytest'], 'conda_build': ['conda-build']} for dep_lst in cast(dict[str, list[str]], data["project"].get("optional-dependencies", {})).values(): for dep_name in dep_lst: deps.add(new_project_dependency(dep_name, DependencySection.RUN_CONSTRAINTS)) return deps