Skip to content

Commit e1feddd

Browse files
Merge pull request #204 from NatPRoach/add_lockfile_metadata
Adding options to log addition additional metadata into the lockfile
2 parents 7c935b9 + 16e32c8 commit e1feddd

13 files changed

+569
-48
lines changed

.pre-commit-config.yaml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@ exclude: ^conda_lock/_vendor/.*$
22

33
repos:
44
- repo: https://github.com/pre-commit/pre-commit-hooks
5-
rev: v4.1.0
5+
rev: v4.3.0
66
hooks:
77
- id: trailing-whitespace
88
exclude: "^.*\\.patch$"
99
- id: check-ast
1010

1111
- repo: https://github.com/psf/black
12-
rev: 22.3.0
12+
rev: 22.10.0
1313
hooks:
1414
- id: black
1515
language_version: python3
1616

17-
- repo: https://gitlab.com/pycqa/flake8
18-
rev: 3.9.2
17+
- repo: https://github.com/pycqa/flake8
18+
rev: 5.0.4
1919
hooks:
2020
- id: flake8
2121

@@ -26,8 +26,8 @@ repos:
2626
args: ["--profile", "black", "--filter-files"]
2727

2828
- repo: https://github.com/pre-commit/mirrors-mypy
29-
rev: v0.931
29+
rev: v0.991
3030
hooks:
3131
- id: mypy
32-
additional_dependencies: [types-filelock, types-requests, types-toml, types-PyYAML, types-setuptools, pydantic]
32+
additional_dependencies: [types-filelock, types-requests, types-toml, types-PyYAML, types-freezegun, types-setuptools, pydantic]
3333
exclude: ^tests/test-local-pip/setup.py$

conda_lock/conda_lock.py

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import pathlib
99
import posixpath
1010
import re
11-
import subprocess
1211
import sys
1312
import tempfile
1413

@@ -34,7 +33,7 @@
3433
import pkg_resources
3534
import yaml
3635

37-
from ensureconda import ensureconda
36+
from ensureconda.api import ensureconda
3837
from typing_extensions import Literal
3938

4039
from conda_lock.click_helpers import OrderedGroup
@@ -65,10 +64,14 @@
6564
from conda_lock.lookup import set_lookup_location
6665
from conda_lock.src_parser import (
6766
Dependency,
67+
GitMeta,
68+
InputMeta,
6869
LockedDependency,
6970
Lockfile,
7071
LockMeta,
7172
LockSpecification,
73+
MetadataOption,
74+
TimeMeta,
7275
UpdateSpecification,
7376
aggregate_lock_specs,
7477
)
@@ -292,6 +295,8 @@ def make_lock_files(
292295
filter_categories: bool = True,
293296
extras: Optional[AbstractSet[str]] = None,
294297
check_input_hash: bool = False,
298+
metadata_choices: AbstractSet[MetadataOption] = frozenset(),
299+
metadata_yamls: Sequence[pathlib.Path] = (),
295300
) -> None:
296301
"""
297302
Generate a lock file from the src files provided
@@ -325,6 +330,10 @@ def make_lock_files(
325330
Filter out unused categories prior to solving
326331
check_input_hash :
327332
Do not re-solve for each target platform for which specifications are unchanged
333+
metadata_choices:
334+
Set of selected metadata fields to generate for this lockfile.
335+
metadata_yamls:
336+
YAML or JSON file(s) containing structured metadata to add to metadata section of the lockfile.
328337
"""
329338

330339
# initialize virtual package fake
@@ -401,10 +410,16 @@ def make_lock_files(
401410
platforms=platforms_to_lock,
402411
lockfile_path=lockfile_path,
403412
update_spec=update_spec,
413+
metadata_choices=metadata_choices,
414+
metadata_yamls=metadata_yamls,
404415
)
405416

406417
if "lock" in kinds:
407-
write_conda_lock_file(lock_content, lockfile_path)
418+
write_conda_lock_file(
419+
lock_content,
420+
lockfile_path,
421+
metadata_choices=metadata_choices,
422+
)
408423
print(
409424
" - Install lock using:",
410425
KIND_USE_TEXT["lock"].format(lockfile=str(lockfile_path)),
@@ -725,13 +740,43 @@ def _solve_for_arch(
725740
return list(conda_deps.values()) + list(pip_deps.values())
726741

727742

743+
def convert_structured_metadata_yaml(in_path: pathlib.Path) -> Dict[str, Any]:
744+
with in_path.open("r") as infile:
745+
metadata = yaml.safe_load(infile)
746+
return metadata
747+
748+
749+
def update_metadata(to_change: Dict[str, Any], change_source: Dict[str, Any]) -> None:
750+
for key in change_source:
751+
if key in to_change:
752+
logger.warning(
753+
f"Custom metadata field {key} provided twice, overwriting value "
754+
+ f"{to_change[key]} with {change_source[key]}"
755+
)
756+
to_change.update(change_source)
757+
758+
759+
def get_custom_metadata(
760+
metadata_yamls: Sequence[pathlib.Path],
761+
) -> Optional[Dict[str, str]]:
762+
custom_metadata_dict: Dict[str, str] = {}
763+
for yaml_path in metadata_yamls:
764+
new_metadata = convert_structured_metadata_yaml(yaml_path)
765+
update_metadata(custom_metadata_dict, new_metadata)
766+
if custom_metadata_dict:
767+
return custom_metadata_dict
768+
return None
769+
770+
728771
def create_lockfile_from_spec(
729772
*,
730773
conda: PathLike,
731774
spec: LockSpecification,
732775
platforms: List[str] = [],
733776
lockfile_path: pathlib.Path,
734777
update_spec: Optional[UpdateSpecification] = None,
778+
metadata_choices: AbstractSet[MetadataOption] = frozenset(),
779+
metadata_yamls: Sequence[pathlib.Path] = (),
735780
) -> Lockfile:
736781
"""
737782
Solve or update specification
@@ -754,13 +799,49 @@ def create_lockfile_from_spec(
754799
for dep in deps:
755800
locked[(dep.manager, dep.name, dep.platform)] = dep
756801

802+
spec_sources: Dict[str, pathlib.Path] = {}
803+
for source in spec.sources:
804+
try:
805+
path = relative_path(lockfile_path.parent, source)
806+
except ValueError as e:
807+
if "Paths don't have the same drive" not in str(e):
808+
raise e
809+
path = str(source.resolve())
810+
spec_sources[path] = source
811+
812+
if MetadataOption.TimeStamp in metadata_choices:
813+
time_metadata = TimeMeta.create()
814+
else:
815+
time_metadata = None
816+
817+
git_metadata = GitMeta.create(
818+
metadata_choices=metadata_choices,
819+
src_files=spec.sources,
820+
)
821+
822+
if metadata_choices & {MetadataOption.InputSha, MetadataOption.InputMd5}:
823+
inputs_metadata: Optional[Dict[str, InputMeta]] = {
824+
relative_path: InputMeta.create(
825+
metadata_choices=metadata_choices, src_file=src_file
826+
)
827+
for relative_path, src_file in spec_sources.items()
828+
}
829+
else:
830+
inputs_metadata = None
831+
832+
custom_metadata = get_custom_metadata(metadata_yamls=metadata_yamls)
833+
757834
return Lockfile(
758835
package=[locked[k] for k in locked],
759836
metadata=LockMeta(
760837
content_hash=spec.content_hash(),
761838
channels=[c for c in spec.channels],
762839
platforms=spec.platforms,
763840
sources=[str(source.resolve()) for source in spec.sources],
841+
git_metadata=git_metadata,
842+
time_metadata=time_metadata,
843+
inputs_metadata=inputs_metadata,
844+
custom_metadata=custom_metadata,
764845
),
765846
)
766847

@@ -939,6 +1020,8 @@ def run_lock(
9391020
virtual_package_spec: Optional[pathlib.Path] = None,
9401021
update: Optional[List[str]] = None,
9411022
filter_categories: bool = False,
1023+
metadata_choices: AbstractSet[MetadataOption] = frozenset(),
1024+
metadata_yamls: Sequence[pathlib.Path] = (),
9421025
) -> None:
9431026
if environment_files == DEFAULT_FILES:
9441027
if lockfile_path.exists():
@@ -983,6 +1066,8 @@ def run_lock(
9831066
extras=extras,
9841067
check_input_hash=check_input_hash,
9851068
filter_categories=filter_categories,
1069+
metadata_choices=metadata_choices,
1070+
metadata_yamls=metadata_yamls,
9861071
)
9871072

9881073

@@ -1114,10 +1199,29 @@ def main() -> None:
11141199
type=str,
11151200
help="Location of the lookup file containing Pypi package names to conda names.",
11161201
)
1202+
@click.option(
1203+
"--md",
1204+
"--metadata",
1205+
"metadata_choices",
1206+
default=[],
1207+
multiple=True,
1208+
type=click.Choice([md.value for md in MetadataOption]),
1209+
help="Metadata fields to include in lock-file",
1210+
)
1211+
@click.option(
1212+
"--mdy",
1213+
"--metadata-yaml",
1214+
"--metadata-json",
1215+
"metadata_yamls",
1216+
default=[],
1217+
multiple=True,
1218+
type=click.Path(),
1219+
help="YAML or JSON file(s) containing structured metadata to add to metadata section of the lockfile.",
1220+
)
11171221
@click.pass_context
11181222
def lock(
11191223
ctx: click.Context,
1120-
conda: Optional[PathLike],
1224+
conda: Optional[str],
11211225
mamba: bool,
11221226
micromamba: bool,
11231227
platform: List[str],
@@ -1133,9 +1237,11 @@ def lock(
11331237
check_input_hash: bool,
11341238
log_level: TLogLevel,
11351239
pdb: bool,
1136-
virtual_package_spec: Optional[PathLike],
1240+
virtual_package_spec: Optional[pathlib.Path],
11371241
pypi_to_conda_lookup_file: Optional[str],
11381242
update: Optional[List[str]] = None,
1243+
metadata_choices: Sequence[str] = (),
1244+
metadata_yamls: Sequence[pathlib.Path] = (),
11391245
) -> None:
11401246
"""Generate fully reproducible lock files for conda environments.
11411247
@@ -1155,6 +1261,10 @@ def lock(
11551261
if pypi_to_conda_lookup_file:
11561262
set_lookup_location(pypi_to_conda_lookup_file)
11571263

1264+
metadata_enum_choices = set(MetadataOption(md) for md in metadata_choices)
1265+
1266+
metadata_yamls = [pathlib.Path(path) for path in metadata_yamls]
1267+
11581268
# bail out if we do not encounter the default file if no files were passed
11591269
if ctx.get_parameter_source("files") == click.core.ParameterSource.DEFAULT:
11601270
candidates = list(files)
@@ -1199,6 +1309,8 @@ def lock(
11991309
virtual_package_spec=virtual_package_spec,
12001310
update=update,
12011311
filter_categories=filter_categories,
1312+
metadata_choices=metadata_enum_choices,
1313+
metadata_yamls=metadata_yamls,
12021314
)
12031315
if strip_auth:
12041316
with tempfile.TemporaryDirectory() as tempdir:

conda_lock/conda_solver.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import logging
33
import os
44
import pathlib
5-
import re
65
import shlex
76
import subprocess
87
import sys
@@ -420,7 +419,7 @@ def update_specs_for_arch(
420419
)
421420
)
422421
}
423-
spec_for_name = {MatchSpec(v).name: v for v in specs}
422+
spec_for_name = {MatchSpec(v).name: v for v in specs} # type: ignore
424423
to_update = [
425424
spec_for_name[name] for name in set(installed).intersection(update)
426425
]

conda_lock/invoke_conda.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from distutils.version import LooseVersion
1111
from typing import IO, Dict, Iterator, List, Optional, Sequence, Union
1212

13-
import ensureconda
13+
from ensureconda.api import determine_micromamba_version, ensureconda
1414

1515
from conda_lock.models.channel import Channel
1616

@@ -26,15 +26,17 @@ def _ensureconda(
2626
micromamba: bool = False,
2727
conda: bool = False,
2828
conda_exe: bool = False,
29-
) -> Optional[PathLike]:
30-
_conda_exe = ensureconda.ensureconda(
29+
) -> Optional[pathlib.Path]:
30+
_conda_exe = ensureconda(
3131
mamba=mamba,
3232
micromamba=micromamba,
3333
conda=conda,
3434
conda_exe=conda_exe,
3535
)
3636

37-
return _conda_exe
37+
if _conda_exe is None:
38+
return None
39+
return pathlib.Path(_conda_exe)
3840

3941

4042
def _determine_conda_executable(
@@ -54,9 +56,7 @@ def determine_conda_executable(
5456
for candidate in _determine_conda_executable(conda_executable, mamba, micromamba):
5557
if candidate is not None:
5658
if is_micromamba(candidate):
57-
if ensureconda.api.determine_micromamba_version(
58-
str(candidate)
59-
) < LooseVersion("0.17"):
59+
if determine_micromamba_version(str(candidate)) < LooseVersion("0.17"):
6060
mamba_root_prefix()
6161
return candidate
6262
raise RuntimeError("Could not find conda (or compatible) executable")

0 commit comments

Comments
 (0)