Source code for conda_tasks.parsers.condarc
"""Parser for .condarc task definitions.
Reads task definitions from the ``plugins.conda_tasks.tasks`` section
of ``.condarc`` using conda's configuration loading API. This means tasks
defined in any condarc source (user, system, environment) are automatically
discovered.
Writing (add/remove) still uses direct YAML manipulation since the config
API is read-only.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from conda.common.serialize.yaml import YAMLError
from conda.common.serialize.yaml import dumps as yaml_dumps
from conda.common.serialize.yaml import loads as yaml_loads
from ..exceptions import TaskNotFoundError, TaskParseError
from .base import TaskFileParser
from .normalize import normalize_tasks
if TYPE_CHECKING:
from pathlib import Path
from typing import Any, ClassVar
from ..models import Task
_CONDARC_KEY = "conda_tasks"
def _raw_tasks_from_condarc() -> dict[str, Any]:
"""Extract raw task definitions from all condarc sources via conda's config API.
Returns a merged dict of ``{task_name: definition}`` from every condarc
source that defines ``plugins.conda_tasks.tasks``.
"""
from conda.base.context import context
merged: dict[str, Any] = {}
for _source, data in context.plugins.raw_data.items():
raw_param = data.get(_CONDARC_KEY)
if raw_param is None:
continue
try:
raw_value = raw_param._raw_value
except AttributeError:
continue
if not isinstance(raw_value, dict):
continue
tasks = raw_value.get("tasks")
if isinstance(tasks, dict):
merged.update(tasks)
return merged
[docs]
class CondaRCParser(TaskFileParser):
"""Reads task definitions from ``.condarc`` via conda's config API."""
extensions: ClassVar[tuple[str, ...]] = ()
filenames: ClassVar[tuple[str, ...]] = (".condarc",)
[docs]
def can_handle(self, path: Path) -> bool:
"""Return True if *path* is a ``.condarc`` with task definitions."""
if path.name not in self.filenames:
return False
try:
data = yaml_loads(path.read_text(encoding="utf-8")) or {}
except YAMLError:
return False
plugins = data.get("plugins", {})
if not isinstance(plugins, dict):
return False
section = plugins.get("conda_tasks") or plugins.get("conda-tasks")
return bool(isinstance(section, dict) and section.get("tasks"))
[docs]
def parse(self, path: Path) -> dict[str, Task]:
"""Parse tasks from condarc via the config API, falling back to direct YAML."""
raw_tasks = _raw_tasks_from_condarc()
if not raw_tasks:
try:
data = yaml_loads(path.read_text(encoding="utf-8")) or {}
except YAMLError as exc:
raise TaskParseError(str(path), str(exc)) from exc
plugins = data.get("plugins", {})
section = plugins.get("conda_tasks") or plugins.get("conda-tasks") or {}
raw_tasks = section.get("tasks", {})
if not isinstance(raw_tasks, dict):
raise TaskParseError(
str(path), "plugins.conda_tasks.tasks must be a mapping"
)
return normalize_tasks(raw_tasks)
[docs]
def add_task(self, path: Path, name: str, task: Task) -> None:
"""Add or update a task under ``plugins.conda_tasks.tasks`` in ``.condarc``."""
if path.exists():
data = yaml_loads(path.read_text(encoding="utf-8")) or {}
else:
data = {}
section = (
data.setdefault("plugins", {})
.setdefault("conda_tasks", {})
.setdefault("tasks", {})
)
defn: dict[str, object] = {}
if task.cmd is not None:
defn["cmd"] = task.cmd
if task.depends_on:
defn["depends-on"] = [d.task for d in task.depends_on]
if task.description:
defn["description"] = task.description
section[name] = defn if defn else task.cmd
path.write_text(yaml_dumps(data), encoding="utf-8")
[docs]
def remove_task(self, path: Path, name: str) -> None:
"""Remove a task from the ``.condarc`` file by name."""
data = yaml_loads(path.read_text(encoding="utf-8")) or {}
plugins = data.get("plugins", {})
section = plugins.get("conda_tasks", plugins.get("conda-tasks", {})).get(
"tasks", {}
)
if name not in section:
raise TaskNotFoundError(name, list(section.keys()))
del section[name]
path.write_text(yaml_dumps(data), encoding="utf-8")