Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions conda/cli/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from boltons.setutils import IndexedSet

from ..auxlib.ish import dals
from ..base.constants import (
REPODATA_FN,
ROOT_ENV_NAME,
Expand All @@ -33,6 +34,7 @@
from ..core.solve import diff_for_unlink_link_precs
from ..deprecations import deprecated
from ..exceptions import (
CondaError,
CondaEnvException,
CondaExitZero,
CondaImportError,
Expand All @@ -42,6 +44,7 @@
CondaValueError,
DirectoryNotACondaEnvironmentError,
DryRunExit,
InvalidInstaller,
NoBaseEnvironmentError,
PackageNotInstalledError,
PackagesNotFoundError,
Expand Down Expand Up @@ -425,6 +428,23 @@ def install(args, parser, command="install"):

handle_txn(unlink_link_transaction, prefix, args, newenv)

# install all other external packages
for installer_type, pkg_specs in env.external_packages.items():
try:
installer = context.plugin_manager.get_installer(installer_type)
installer.install(prefix, pkg_specs, env.config)
except InvalidInstaller:
raise CondaError(
dals(
f"""
Unable to install package for {installer_type}.

Please double check and ensure your dependencies for {installer_type}
are correctly formatted.
"""
)
)


def install_clone(args, parser):
"""Executes an install of a new conda environment by cloning."""
Expand Down
16 changes: 9 additions & 7 deletions conda/cli/main_env_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def configure_parser(sub_parsers: _SubParsersAction, **kwargs) -> ArgumentParser
def execute(args: Namespace, parser: ArgumentParser) -> int:
from ..auxlib.ish import dals
from ..base.context import context, determine_target_prefix
from ..common.constants import NULL
from ..common.serialize import json
from ..core.prefix_data import PrefixData
from ..env.env import print_result
Expand Down Expand Up @@ -166,36 +167,37 @@ def execute(args: Namespace, parser: ArgumentParser) -> int:
context.create_default_packages if not args.no_default_packages else []
)

cli_args = {k: v for k,v in vars(args).items() if v != NULL and v != None}

if args.dry_run:
installer_type = "conda"
installer = get_installer(installer_type)

pkg_specs = [*env.requested_packages, *args_packages]

solved_env = installer.dry_run(pkg_specs, args, env)
solved_env = installer.dry_run(env.prefix, pkg_specs, env.config, **cli_args)
if args.json:
print(json.dumps(solved_env.to_dict()))
else:
print(solved_env.to_yaml(), end="")

else:
# Install conda packages
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Installing conda packages does not touch the plugin system.

if args_packages:
installer_type = "conda"
installer = get_installer(installer_type)
result[installer_type] = installer.install(prefix, args_packages, args, env)

# install conda packages
result[installer_type] = installer.install(prefix, args_packages, env.config, **cli_args)
installer_type = "conda"
installer = get_installer(installer_type)
result[installer_type] = installer.install(
prefix, env.requested_packages, args, env
prefix, env.requested_packages, env.config, **cli_args
)

# install all other external packages
for installer_type, pkg_specs in env.external_packages.items():
try:
installer = get_installer(installer_type)
result[installer_type] = installer.install(prefix, pkg_specs, args, env)
installer = context.plugin_manager.get_installer(installer_type)
result[installer_type] = installer.install(prefix, pkg_specs, env.config, **cli_args)
except InvalidInstaller:
raise CondaError(
dals(
Expand Down
2 changes: 1 addition & 1 deletion conda/env/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ def to_environment_model(self) -> EnvironmentModel:
platform=context.subdir,
name=self.name,
config=config,
variables=self.variables,
variables=self.variables or {},
external_packages=external_packages,
requested_packages=requested_packages,
)
Expand Down
40 changes: 19 additions & 21 deletions conda/env/installers/conda.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@
from ...env.env import EnvironmentYaml
from ...exceptions import CondaValueError, UnsatisfiableError
from ...models.channel import Channel, prioritize_channels
from ...models.records import PackageRecord

if TYPE_CHECKING:
from argparse import Namespace

from ...core.solve import Solver
from ...models.environment import Environment
from ...models.environment import EnvironmentConfig


def _solve(
prefix: str, specs: list[str], args: Namespace, env: Environment, *_, **kwargs
prefix: str, specs: list[str], env_config: EnvironmentConfig, **kwargs
) -> Solver:
"""Solve the environment.

Expand All @@ -37,9 +38,9 @@ def _solve(
"""
# TODO: support all various ways this happens
# Including 'nodefaults' in the channels list disables the defaults
channel_urls = [chan for chan in env.config.channels if chan != "nodefaults"]
channel_urls = [chan for chan in env_config.channels if chan != "nodefaults"]

if "nodefaults" not in env.config.channels:
if "nodefaults" not in env_config.channels:
channel_urls.extend(context.channels)
_channel_priority_map = prioritize_channels(channel_urls)

Expand All @@ -54,7 +55,7 @@ def _solve(


def dry_run(
specs: list[str], args: Namespace, env: Environment, *_, **kwargs
prefix: str, specs: list[str], env_config: EnvironmentConfig, **kwargs
) -> EnvironmentYaml:
"""Do a dry run of the environment solve.

Expand All @@ -64,16 +65,16 @@ def dry_run(
:return: Solved environment object
:rtype: EnvironmentYaml
"""
solver = _solve(tempfile.mkdtemp(), specs, args, env, *_, **kwargs)
solver = _solve(prefix, specs, env_config, **kwargs)
pkgs = solver.solve_final_state()
return EnvironmentYaml(
name=env.name, dependencies=[str(p) for p in pkgs], channels=env.config.channels
name=prefix, dependencies=[str(p) for p in pkgs], channels=env_config.channels
)


def install(
prefix: str, specs: list[str], args: Namespace, env: Environment, *_, **kwargs
) -> dict | None:
prefix: str, specs: list, env_config: EnvironmentConfig, **kwargs
) -> dict:
"""Install packages into a conda environment.

This function handles two main paths:
Expand All @@ -93,27 +94,24 @@ def install(
processed, the conda client SHOULD NOT invoke a solver."
"""
# Handle explicit environments separately per CEP-23 requirements
if env.explicit_packages:
if not specs:
return {}

# If the specs are package records, these are explicit packages.
if isinstance(specs[0], PackageRecord):
from ...misc import install_explicit_packages

# For explicit environments, we consider any provided specs as user-requested
# All packages in the explicit file are installed, but only user-provided specs
# are recorded in history as explicitly requested
requested_specs = specs if specs else ()

# Install explicit packages - bypassing the solver completely
return install_explicit_packages(
package_cache_records=env.explicit_packages,
package_cache_records=specs,
prefix=prefix,
requested_specs=requested_specs,
)

# For regular environments, proceed with the normal solve-based installation
solver = _solve(prefix, specs, args, env, *_, **kwargs)
solver = _solve(prefix, specs, env_config, **kwargs)

try:
unlink_link_transaction = solver.solve_for_transaction(
prune=getattr(args, "prune", False),
prune=getattr(kwargs, "prune", False),
update_modifier=UpdateModifier.FREEZE_INSTALLED,
)
except (UnsatisfiableError, SystemExit) as exc:
Expand All @@ -122,7 +120,7 @@ def install(
if not getattr(exc, "allow_retry", True):
raise
unlink_link_transaction = solver.solve_for_transaction(
prune=getattr(args, "prune", False), update_modifier=NULL
prune=getattr(kwargs, "prune", False), update_modifier=NULL
)
# Execute the transaction and return success
if unlink_link_transaction.nothing_to_do:
Expand Down
24 changes: 11 additions & 13 deletions conda/env/installers/pip.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
from ...env.pip_util import get_pip_installed_packages, pip_subprocess
from ...gateways.connection.session import CONDA_SESSION_SCHEMES
from ...reporters import get_spinner
from ...models.environment import EnvironmentConfig


log = getLogger(__name__)


def _pip_install_via_requirements(prefix, specs, args, *_, **kwargs):
def _pip_install_via_requirements(prefix, specs):
"""
Installs the pip dependencies in specs using a temporary pip requirements file.

Expand All @@ -28,16 +30,7 @@ def _pip_install_via_requirements(prefix, specs, args, *_, **kwargs):
See: https://pip.pypa.io/en/stable/user_guide/#requirements-files
https://pip.pypa.io/en/stable/reference/pip_install/#requirements-file-format
"""
url_scheme = args.file.split("://", 1)[0]
if url_scheme in CONDA_SESSION_SCHEMES:
pip_workdir = None
else:
try:
pip_workdir = op.dirname(op.abspath(args.file))
if not os.access(pip_workdir, os.W_OK):
pip_workdir = None
except AttributeError:
pip_workdir = None
pip_workdir = None
requirements = None
try:
# Generate the temporary requirements file
Expand Down Expand Up @@ -67,6 +60,11 @@ def _pip_install_via_requirements(prefix, specs, args, *_, **kwargs):
return get_pip_installed_packages(stdout)


def install(*args, **kwargs):
def install(prefix: str, specs: list[str], *args, **kwargs):
with get_spinner("Installing pip dependencies"):
return _pip_install_via_requirements(*args, **kwargs)
return _pip_install_via_requirements(prefix, specs)


def dry_run(prefix: str, specs: list[str], *args, **kwargs) -> dict:
print("pretending to (dry-run) install stuff with pip")
return {}
20 changes: 9 additions & 11 deletions conda/models/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,16 +500,12 @@ def from_cli(
# environment spec plugin. The core conda cli commands are not
# ready for that yet. So, use this old way of reading specs from
# files.
for fpath in args.file:
try:
specs.extend(
[spec for spec in specs_from_url(fpath) if spec != EXPLICIT_MARKER]
)
except UnicodeError:
raise CondaError(
"Error reading file, file should be a text file containing packages\n"
"See `conda create --help` for details."
)
file_envs = []
for path in args.file:
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for the purposes of demoing conda env create and conda create being used (almost) interchangeably.

# parse the file
spec_hook = context.plugin_manager.get_environment_specifier(path)
file_env = spec_hook.environment_spec(path).env
file_envs.append(file_env)

# Add default packages if required. If the default package is already
# present in the list of specs, don't add it (this will override any
Expand Down Expand Up @@ -538,11 +534,13 @@ def from_cli(
"Cannot mix specifications with conda package filenames"
)

return Environment(
cli_env = Environment(
name=args.name,
prefix=context.target_prefix,
platform=context.subdir,
requested_packages=requested_packages,
explicit_packages=explicit_packages,
config=EnvironmentConfig.from_context(),
)

return Environment.merge(cli_env, *file_envs)
1 change: 1 addition & 0 deletions conda/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
CondaAuthHandler,
CondaEnvironmentSpecifier,
CondaHealthCheck,
CondaInstaller,
CondaPostCommand,
CondaPostSolve,
CondaPostTransactionAction,
Expand Down
5 changes: 5 additions & 0 deletions conda/plugins/hookspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
CondaEnvironmentExporter,
CondaEnvironmentSpecifier,
CondaHealthCheck,
CondaInstaller,
CondaPostCommand,
CondaPostSolve,
CondaPostTransactionAction,
Expand Down Expand Up @@ -730,3 +731,7 @@ def conda_environment_exporters():
)
"""
yield from ()

@_hookspec
def conda_installers(self) -> Iterable[CondaInstaller]:
yield from ()
8 changes: 8 additions & 0 deletions conda/plugins/installers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copyright (C) 2012 Anaconda, Inc
# SPDX-License-Identifier: BSD-3-Clause
"""Register the built-in env_installer hook implementations."""

from . import pip, uv

#: The list of env_installer plugins for easier registration with pluggy
plugins = [pip, uv]
19 changes: 19 additions & 0 deletions conda/plugins/installers/pip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright (C) 2012 Anaconda, Inc
# SPDX-License-Identifier: BSD-3-Clause
"""Register the pip installer for conda env files."""

from .. import CondaInstaller, hookimpl


@hookimpl
def conda_installers():
from ...env.installers.pip import dry_run as pip_dry_run
from ...env.installers.pip import install as pip_install

# In this demo, swap the meaning of "pip" and "pypi"
yield CondaInstaller(
name="pypi_pip",
types=("pip","pypi"),
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These types are mapping to the Environment.external_packages dict. The environment.yaml environment spec plugin will put all pip depenencies in the pip key of that dict. This relationship is not well definied. It would be better if it could be more strict.

install=pip_install,
dry_run=pip_dry_run,
)
29 changes: 29 additions & 0 deletions conda/plugins/installers/uv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright (C) 2012 Anaconda, Inc
# SPDX-License-Identifier: BSD-3-Clause
"""Register the pip installer for conda env files."""

from .. import CondaInstaller, hookimpl
from ...models.environment import EnvironmentConfig

def uv_install(prefix: str, specs: list[str], *args, **kwargs) -> dict:
print("pretending to install stuff with uv")
for spec in specs:
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clearly not actually implementing an install with uv. It's helpful to have this output for visual confirmation that the right plugins are getting invoked.

print(f" - {spec}")
return {}


def uv_dry_run(prefix: str, specs: list[str], *args, **kwargs) -> dict:
print("pretending to (dry-run) install stuff with uv")
for spec in specs:
print(f" - {spec}")
return {}


@hookimpl
def conda_installers():
yield CondaInstaller(
name="pypi_uv",
types=("uv", "pip", "pypi"),
install=uv_install,
dry_run=uv_dry_run,
)
Loading
Loading