Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor lock command and fix an edge case #695

Merged
merged 10 commits into from
Sep 13, 2024
148 changes: 83 additions & 65 deletions conda_lock/conda_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@


logger = logging.getLogger(__name__)
DEFAULT_FILES = [pathlib.Path("environment.yml")]
DEFAULT_FILES = [pathlib.Path("environment.yml"), pathlib.Path("environment.yaml")]

# Captures basic auth credentials, if they exists, in the third capture group.
AUTH_PATTERN = re.compile(r"^(# pip .* @ )?(https?:\/\/)(.*:.*@)?(.*)")
Expand Down Expand Up @@ -256,11 +256,11 @@ def make_lock_files( # noqa: C901
conda: PathLike,
src_files: List[pathlib.Path],
kinds: Sequence[TKindAll],
lockfile_path: pathlib.Path = pathlib.Path(DEFAULT_LOCKFILE_NAME),
lockfile_path: Optional[pathlib.Path] = None,
platform_overrides: Optional[Sequence[str]] = None,
channel_overrides: Optional[Sequence[str]] = None,
virtual_package_spec: Optional[pathlib.Path] = None,
update: Optional[List[str]] = None,
update: Optional[Sequence[str]] = None,
include_dev_dependencies: bool = True,
filename_template: Optional[str] = None,
filter_categories: bool = False,
Expand Down Expand Up @@ -328,6 +328,8 @@ def make_lock_files( # noqa: C901

# Load existing lockfile if it exists
original_lock_content: Optional[Lockfile] = None
if lockfile_path is None:
lockfile_path = pathlib.Path(DEFAULT_LOCKFILE_NAME)
if lockfile_path.exists():
try:
original_lock_content = parse_conda_lock_file(lockfile_path)
Expand Down Expand Up @@ -356,6 +358,9 @@ def make_lock_files( # noqa: C901
platforms_already_locked: List[str] = []
if original_lock_content is not None:
platforms_already_locked = list(original_lock_content.metadata.platforms)
if update is not None:
# Narrow `update` sequence to list for mypy
update = list(update)
update_spec = UpdateSpecification(
locked=original_lock_content.package, update=update
)
Expand Down Expand Up @@ -1052,59 +1057,84 @@ def _detect_lockfile_kind(path: pathlib.Path) -> TKindAll:
)


def handle_no_specified_source_files(
lockfile_path: Optional[pathlib.Path],
) -> List[pathlib.Path]:
"""No sources were specified on the CLI, so try to read them from the lockfile.

If none are found, then fall back to the default files.
"""
if lockfile_path is None:
lockfile_path = pathlib.Path(DEFAULT_LOCKFILE_NAME)
if lockfile_path.exists():
lock_content = parse_conda_lock_file(lockfile_path)
# reconstruct native paths
locked_environment_files = [
(
pathlib.Path(p)
# absolute paths could be locked for both flavours
if pathlib.PurePosixPath(p).is_absolute()
or pathlib.PureWindowsPath(p).is_absolute()
else pathlib.Path(
pathlib.PurePosixPath(lockfile_path).parent
/ pathlib.PurePosixPath(p)
)
)
for p in lock_content.metadata.sources
]
if all(p.exists() for p in locked_environment_files):
environment_files = locked_environment_files
logger.warning(
f"Using source files {[str(p) for p in locked_environment_files]} "
f"from {lockfile_path} to create the environment."
)
else:
missing = [p for p in locked_environment_files if not p.exists()]
environment_files = DEFAULT_FILES.copy()
print(
f"{lockfile_path} was created from {[str(p) for p in locked_environment_files]},"
f" but some files ({[str(p) for p in missing]}) do not exist. Falling back to"
f" {[str(p) for p in environment_files]}.",
file=sys.stderr,
)
else:
# No lockfile provided, so fall back to the default files
environment_files = [f for f in DEFAULT_FILES if f.exists()]
if len(environment_files) == 0:
logger.error(
"No source files provided and no default files found. Exiting."
)
sys.exit(1)
elif len(environment_files) > 1:
logger.error(f"Multiple default files found: {environment_files}. Exiting.")
sys.exit(1)
return environment_files


def run_lock(
environment_files: List[pathlib.Path],
*,
conda_exe: Optional[PathLike],
platforms: Optional[List[str]] = None,
platforms: Optional[Sequence[str]] = None,
mamba: bool = False,
micromamba: bool = False,
include_dev_dependencies: bool = True,
channel_overrides: Optional[Sequence[str]] = None,
filename_template: Optional[str] = None,
kinds: Optional[Sequence[TKindAll]] = None,
lockfile_path: pathlib.Path = pathlib.Path(DEFAULT_LOCKFILE_NAME),
lockfile_path: Optional[pathlib.Path] = None,
check_input_hash: bool = False,
extras: Optional[AbstractSet[str]] = None,
virtual_package_spec: Optional[pathlib.Path] = None,
with_cuda: Optional[str] = None,
update: Optional[List[str]] = None,
update: Optional[Sequence[str]] = None,
filter_categories: bool = False,
metadata_choices: AbstractSet[MetadataOption] = frozenset(),
metadata_yamls: Sequence[pathlib.Path] = (),
strip_auth: bool = False,
) -> None:
if environment_files == DEFAULT_FILES:
if lockfile_path.exists():
lock_content = parse_conda_lock_file(lockfile_path)
# reconstruct native paths
locked_environment_files = [
(
pathlib.Path(p)
# absolute paths could be locked for both flavours
if pathlib.PurePosixPath(p).is_absolute()
or pathlib.PureWindowsPath(p).is_absolute()
else pathlib.Path(
pathlib.PurePosixPath(lockfile_path).parent
/ pathlib.PurePosixPath(p)
)
)
for p in lock_content.metadata.sources
]
if all(p.exists() for p in locked_environment_files):
environment_files = locked_environment_files
else:
missing = [p for p in locked_environment_files if not p.exists()]
print(
f"{lockfile_path} was created from {[str(p) for p in locked_environment_files]},"
f" but some files ({[str(p) for p in missing]}) do not exist. Falling back to"
f" {[str(p) for p in environment_files]}.",
file=sys.stderr,
)
else:
long_ext_file = pathlib.Path("environment.yaml")
if long_ext_file.exists() and not environment_files[0].exists():
environment_files = [long_ext_file]
if len(environment_files) == 0:
environment_files = handle_no_specified_source_files(lockfile_path)

_conda_exe = determine_conda_executable(
conda_exe, mamba=mamba, micromamba=micromamba
Expand Down Expand Up @@ -1184,7 +1214,6 @@ def main() -> None:
"-f",
"--file",
"files",
default=DEFAULT_FILES,
type=click.Path(),
multiple=True,
help="path to a conda environment specification(s)",
Expand All @@ -1204,7 +1233,7 @@ def main() -> None:
)
@click.option(
"--lockfile",
default=DEFAULT_LOCKFILE_NAME,
default=None,
help="Path to a conda-lock.yml to create or update",
)
@click.option(
Expand Down Expand Up @@ -1298,25 +1327,25 @@ def lock(
conda: Optional[str],
mamba: bool,
micromamba: bool,
platform: List[str],
channel_overrides: List[str],
platform: Sequence[str],
channel_overrides: Sequence[str],
dev_dependencies: bool,
files: List[pathlib.Path],
kind: List[Union[Literal["lock"], Literal["env"], Literal["explicit"]]],
files: Sequence[PathLike],
kind: Sequence[Union[Literal["lock"], Literal["env"], Literal["explicit"]]],
filename_template: str,
lockfile: PathLike,
lockfile: Optional[PathLike],
strip_auth: bool,
extras: List[str],
extras: Sequence[str],
filter_categories: bool,
check_input_hash: bool,
log_level: TLogLevel,
pdb: bool,
virtual_package_spec: Optional[pathlib.Path],
virtual_package_spec: Optional[PathLike],
pypi_to_conda_lookup_file: Optional[str],
with_cuda: Optional[str] = None,
update: Optional[List[str]] = None,
update: Optional[Sequence[str]] = None,
metadata_choices: Sequence[str] = (),
metadata_yamls: Sequence[pathlib.Path] = (),
metadata_yamls: Sequence[PathLike] = (),
) -> None:
"""Generate fully reproducible lock files for conda environments.

Expand All @@ -1341,23 +1370,12 @@ def lock(

metadata_enum_choices = set(MetadataOption(md) for md in metadata_choices)

metadata_yamls = [pathlib.Path(path) for path in metadata_yamls]

# bail out if we do not encounter the default file if no files were passed
if ctx.get_parameter_source("files") == click.core.ParameterSource.DEFAULT: # type: ignore
candidates = list(files)
candidates += [f.with_name(f.name.replace(".yml", ".yaml")) for f in candidates]
for f in candidates:
if f.exists():
break
else:
print(ctx.get_help())
sys.exit(1)
environment_files = [pathlib.Path(file) for file in files]

if pdb:
sys.excepthook = _handle_exception_post_mortem

if not virtual_package_spec:
if virtual_package_spec is None:
candidates = [
pathlib.Path("virtual-packages.yml"),
pathlib.Path("virtual-packages.yaml"),
Expand All @@ -1370,26 +1388,25 @@ def lock(
else:
virtual_package_spec = pathlib.Path(virtual_package_spec)

files = [pathlib.Path(file) for file in files]
extras_ = set(extras)
lock_func = partial(
run_lock,
environment_files=files,
environment_files=environment_files,
conda_exe=conda,
platforms=platform,
mamba=mamba,
micromamba=micromamba,
include_dev_dependencies=dev_dependencies,
channel_overrides=channel_overrides,
kinds=kind,
lockfile_path=pathlib.Path(lockfile),
lockfile_path=None if lockfile is None else pathlib.Path(lockfile),
extras=extras_,
virtual_package_spec=virtual_package_spec,
with_cuda=with_cuda,
update=update,
filter_categories=filter_categories,
metadata_choices=metadata_enum_choices,
metadata_yamls=metadata_yamls,
metadata_yamls=[pathlib.Path(path) for path in metadata_yamls],
strip_auth=strip_auth,
)
if strip_auth:
Expand Down Expand Up @@ -1619,6 +1636,7 @@ def render(
# bail out if we do not encounter the lockfile
lock_file = pathlib.Path(lock_file)
if not lock_file.exists():
print(f"ERROR: Lockfile {lock_file} does not exist.\n\n", file=sys.stderr)
print(ctx.get_help())
sys.exit(1)

Expand Down
7 changes: 2 additions & 5 deletions tests/test_conda_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@

from conda_lock import __version__, pypi_solver
from conda_lock.conda_lock import (
DEFAULT_FILES,
DEFAULT_LOCKFILE_NAME,
_add_auth_to_line,
_add_auth_to_lockfile,
Expand Down Expand Up @@ -1446,7 +1445,7 @@ def test_run_lock_with_locked_environment_files(
run_lock([pre_environment], conda_exe="mamba")
make_lock_files = MagicMock()
monkeypatch.setattr("conda_lock.conda_lock.make_lock_files", make_lock_files)
run_lock(DEFAULT_FILES, conda_exe=conda_exe, update=["pydantic"])
run_lock([], conda_exe=conda_exe, update=["pydantic"])
src_files = make_lock_files.call_args.kwargs["src_files"]

assert [p.resolve() for p in src_files] == [
Expand All @@ -1473,9 +1472,7 @@ def test_run_lock_relative_source_path(
assert Path(locked_environment) == Path("../sources/environment.yaml")
make_lock_files = MagicMock()
monkeypatch.setattr("conda_lock.conda_lock.make_lock_files", make_lock_files)
run_lock(
DEFAULT_FILES, lockfile_path=lockfile, conda_exe=conda_exe, update=["pydantic"]
)
run_lock([], lockfile_path=lockfile, conda_exe=conda_exe, update=["pydantic"])
src_files = make_lock_files.call_args.kwargs["src_files"]
assert [p.resolve() for p in src_files] == [environment.resolve()]

Expand Down
Loading