Source code for conda_project.conda

# -*- coding: utf-8 -*-
# Copyright (C) 2022-2024 Anaconda, Inc
# SPDX-License-Identifier: BSD-3-Clause

# Copyright (C) 2022 Anaconda, Inc
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations

import json
import os
import shlex
import signal
import subprocess
from contextlib import nullcontext
from functools import lru_cache
from logging import Logger
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Dict, List, NoReturn, Optional, Tuple, Union, cast

import conda_lock._vendor.conda.gateways.logging  # noqa: F401
import pexpect
from conda_lock._vendor.conda.core.prefix_data import PrefixData, PrefixRecord
from conda_lock._vendor.conda.models.channel import Channel
from conda_lock._vendor.conda.utils import wrap_subprocess_call
from conda_lock.conda_lock import (
    default_virtual_package_repodata,
    make_lock_spec,
    parse_conda_lock_file,
)
from conda_lock.lockfile.v2prelim.models import Lockfile

from .exceptions import CondaProjectError
from .project_file import EnvironmentYaml, UniqueOrderedList
from .utils import Spinner, detect_shell, execvped, is_windows

CONDA_EXE = os.environ.get("CONDA_EXE", "conda")
CONDA_ROOT = os.environ.get("CONDA_ROOT")
CONDA_PREFIX = os.environ.get("CONDA_PREFIX")


[docs] def call_conda( args: List[str], condarc_path: Optional[Path] = None, verbose: bool = False, logger: Optional[Logger] = None, variables: Optional[dict[str, str]] = None, ) -> subprocess.CompletedProcess: """Call conda CLI with subprocess.run""" parent_process_env = os.environ.copy() variables = {} if variables is None else variables env = {**variables, **parent_process_env} if condarc_path is not None: if logger is not None: logger.info(f"setting CONDARC env variable to {condarc_path}") env["CONDARC"] = str(condarc_path) cmd = [CONDA_EXE] + args if verbose: stdout = None else: stdout = subprocess.PIPE if logger is not None: logger.info(f'running conda command: {" ".join(cmd)}') proc = subprocess.run( cmd, env=env, stdout=stdout, stderr=subprocess.PIPE, encoding="utf-8" ) if proc.returncode != 0: print_cmd = " ".join(cmd) raise CondaProjectError(f"Failed to run:\n {print_cmd}\n{proc.stderr.strip()}") return proc
[docs] def is_conda_env(prefix: Path) -> bool: return (prefix / "conda-meta" / "history").exists()
[docs] def conda_prefix(env: Optional[Union[str, Path]] = None) -> Path: """Return the path to a conda environment""" if env is None: return Path(os.environ["CONDA_PREFIX"]).resolve() elif env in ("base", "root"): return Path(os.environ["CONDA_ROOT"]).resolve() else: env = Path(env) if isinstance(env, str) else env if is_conda_env(env): return env.resolve() else: for d in conda_info()["envs_dirs"]: p = Path(d) / env if is_conda_env(p): return p.resolve() else: raise ValueError(f"{env} is not a valid conda environment")
[docs] def env_export( prefix: Path, from_history: bool = True, pin_versions: bool = True, verbose: bool = False, ) -> Tuple[EnvironmentYaml, Lockfile]: """Create an environment.yml spec and lockfile from an existing environment""" pd = PrefixData(prefix, pip_interop_enabled=True) pkgs = cast(List[PrefixRecord], pd.iter_records()) channels: List[Channel] = [] dependencies: List[Union[str, Dict[str, List[str]]]] = [] pip: List[str] = [] n_conda = 0 n_pip = 0 for p in pkgs: if p.schannel == "pypi": n_pip += 1 if from_history: for fn in p.files: fn = prefix / Path(fn) if fn.name == "REQUESTED": pip.append(f"{p.name}=={p.version}") break else: pip.append(f"{p.name}=={p.version}") continue n_conda += 1 if from_history and p.requested_spec == "None": continue if from_history and pin_versions: spec = f"{p.schannel}::{p.name}={p.version}" elif from_history and (not pin_versions): spec = f"{p.schannel}::{p.requested_spec}" else: spec = f"{p.schannel}::{p.name}={p.version}={p.build}" dependencies.append(spec) channels.append(p.channel) full_export = (not from_history) or ( len(pip) + len(dependencies) == n_conda + n_pip ) empty = n_conda + n_pip == 0 if pip: dependencies.append({"pip": pip}) if full_export: subdirs = set(c.subdir for c in channels) subdirs.discard("pypi") subdirs.discard("noarch") if not subdirs: platforms = [current_platform()] else: platforms = list(subdirs) else: platforms = DEFAULT_PLATFORMS channel_names = UniqueOrderedList( [Channel.from_url(url).canonical_name for url in conda_info()["channels"]] ) environment = EnvironmentYaml( name=prefix.name, channels=channel_names, dependencies=dependencies, platforms=DEFAULT_PLATFORMS if empty else platforms, prefix=prefix, ) with Spinner("Constructing lockfile") if verbose else nullcontext(): with TemporaryDirectory() as tmp: tmp = Path(tmp) requested = tmp / "requested.yml" environment.yaml(requested) spec = make_lock_spec( src_files=[requested], virtual_package_repo=default_virtual_package_repodata(), ) if full_export or empty: exported = requested else: exported = tmp / "exported.yml" call_conda( ["env", "export", "-p", str(prefix), "--file", str(exported)] ) lock = tmp / "conda-lock.yml" call_conda( [ "lock", "-f", str(exported), "--lockfile", str(lock), "-p", current_platform(), "--metadata", "timestamp", ] ) lock_content = parse_conda_lock_file(lock) lock_content.metadata.content_hash = spec.content_hash() lock_content.metadata.sources = ["environment.yml"] return environment, lock_content
[docs] def conda_info(): proc = call_conda(["info", "--json"]) parsed = json.loads(proc.stdout) return parsed
[docs] @lru_cache() def current_platform() -> str: """Load the current platform by calling conda info.""" info = conda_info() return info.get("platform")
DEFAULT_PLATFORMS = set( ["osx-64", "win-64", "linux-64", "osx-arm64", current_platform()] )
[docs] def conda_run( cmd: str, prefix: Path, working_dir: Path, env: Optional[Dict[str, str]] = None, extra_args: Optional[List[str]] = None, ) -> NoReturn: extra_args = [] if extra_args is None else extra_args arguments = shlex.split(cmd) + extra_args _, (shell, *args) = wrap_subprocess_call( root_prefix=CONDA_ROOT, prefix=str(prefix), dev_mode=False, debug_wrapper_scripts=False, arguments=arguments, use_system_tmp_path=True, ) env = {} if env is None else env if not is_windows(): args = ["-c", *args] execvped(file=shell, args=args, env=env, cwd=working_dir)
def _send_activation(child_shell: pexpect.spawn, prefix): def sigwinch_passthrough(sig, data): if not child_shell.closed: t = os.get_terminal_size() child_shell.setwinsize(t.lines, t.columns) t = os.get_terminal_size() child_shell.setwinsize(t.lines, t.columns) signal.signal(signal.SIGWINCH, sigwinch_passthrough) child_shell.sendline(f"conda activate {prefix}") child_shell.interact() child_shell.close()
[docs] def conda_activate(prefix: Path, working_dir: Path, env: Optional[Dict] = None): env = {} if env is None else env shell_path, shell_name = detect_shell() args = [] if is_windows(): if shell_name in ["powershell", "pwsh"]: conda_hook = str(Path(CONDA_ROOT) / "shell" / "condabin" / "conda-hook.ps1") args = [ "-ExecutionPolicy", "ByPass", "-NoExit", conda_hook, ";", "conda", "activate", str(prefix), ] elif shell_name == "cmd": activate_bat = str(Path(CONDA_ROOT) / "Scripts" / "activate.bat") args = ["/K", activate_bat, str(prefix)] else: args = ["-i"] activate_message = ( f"## Project environment {prefix.name} activated in a new shell.\n" f"## Exit this shell to de-activate." ) print(activate_message) if is_windows(): subprocess.run([shell_path, *args], cwd=working_dir, env=env) else: child_shell = pexpect.spawn( command=shell_path, args=args, cwd=working_dir, env=env, echo=True ) _send_activation(child_shell, prefix)