8
8
import pathlib
9
9
import posixpath
10
10
import re
11
- import subprocess
12
11
import sys
13
12
import tempfile
14
13
34
33
import pkg_resources
35
34
import yaml
36
35
37
- from ensureconda import ensureconda
36
+ from ensureconda . api import ensureconda
38
37
from typing_extensions import Literal
39
38
40
39
from conda_lock .click_helpers import OrderedGroup
65
64
from conda_lock .lookup import set_lookup_location
66
65
from conda_lock .src_parser import (
67
66
Dependency ,
67
+ GitMeta ,
68
+ InputMeta ,
68
69
LockedDependency ,
69
70
Lockfile ,
70
71
LockMeta ,
71
72
LockSpecification ,
73
+ MetadataOption ,
74
+ TimeMeta ,
72
75
UpdateSpecification ,
73
76
aggregate_lock_specs ,
74
77
)
@@ -292,6 +295,8 @@ def make_lock_files(
292
295
filter_categories : bool = True ,
293
296
extras : Optional [AbstractSet [str ]] = None ,
294
297
check_input_hash : bool = False ,
298
+ metadata_choices : AbstractSet [MetadataOption ] = frozenset (),
299
+ metadata_yamls : Sequence [pathlib .Path ] = (),
295
300
) -> None :
296
301
"""
297
302
Generate a lock file from the src files provided
@@ -325,6 +330,10 @@ def make_lock_files(
325
330
Filter out unused categories prior to solving
326
331
check_input_hash :
327
332
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.
328
337
"""
329
338
330
339
# initialize virtual package fake
@@ -401,10 +410,16 @@ def make_lock_files(
401
410
platforms = platforms_to_lock ,
402
411
lockfile_path = lockfile_path ,
403
412
update_spec = update_spec ,
413
+ metadata_choices = metadata_choices ,
414
+ metadata_yamls = metadata_yamls ,
404
415
)
405
416
406
417
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
+ )
408
423
print (
409
424
" - Install lock using:" ,
410
425
KIND_USE_TEXT ["lock" ].format (lockfile = str (lockfile_path )),
@@ -725,13 +740,43 @@ def _solve_for_arch(
725
740
return list (conda_deps .values ()) + list (pip_deps .values ())
726
741
727
742
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
+
728
771
def create_lockfile_from_spec (
729
772
* ,
730
773
conda : PathLike ,
731
774
spec : LockSpecification ,
732
775
platforms : List [str ] = [],
733
776
lockfile_path : pathlib .Path ,
734
777
update_spec : Optional [UpdateSpecification ] = None ,
778
+ metadata_choices : AbstractSet [MetadataOption ] = frozenset (),
779
+ metadata_yamls : Sequence [pathlib .Path ] = (),
735
780
) -> Lockfile :
736
781
"""
737
782
Solve or update specification
@@ -754,13 +799,49 @@ def create_lockfile_from_spec(
754
799
for dep in deps :
755
800
locked [(dep .manager , dep .name , dep .platform )] = dep
756
801
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
+
757
834
return Lockfile (
758
835
package = [locked [k ] for k in locked ],
759
836
metadata = LockMeta (
760
837
content_hash = spec .content_hash (),
761
838
channels = [c for c in spec .channels ],
762
839
platforms = spec .platforms ,
763
840
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 ,
764
845
),
765
846
)
766
847
@@ -939,6 +1020,8 @@ def run_lock(
939
1020
virtual_package_spec : Optional [pathlib .Path ] = None ,
940
1021
update : Optional [List [str ]] = None ,
941
1022
filter_categories : bool = False ,
1023
+ metadata_choices : AbstractSet [MetadataOption ] = frozenset (),
1024
+ metadata_yamls : Sequence [pathlib .Path ] = (),
942
1025
) -> None :
943
1026
if environment_files == DEFAULT_FILES :
944
1027
if lockfile_path .exists ():
@@ -983,6 +1066,8 @@ def run_lock(
983
1066
extras = extras ,
984
1067
check_input_hash = check_input_hash ,
985
1068
filter_categories = filter_categories ,
1069
+ metadata_choices = metadata_choices ,
1070
+ metadata_yamls = metadata_yamls ,
986
1071
)
987
1072
988
1073
@@ -1114,10 +1199,29 @@ def main() -> None:
1114
1199
type = str ,
1115
1200
help = "Location of the lookup file containing Pypi package names to conda names." ,
1116
1201
)
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
+ )
1117
1221
@click .pass_context
1118
1222
def lock (
1119
1223
ctx : click .Context ,
1120
- conda : Optional [PathLike ],
1224
+ conda : Optional [str ],
1121
1225
mamba : bool ,
1122
1226
micromamba : bool ,
1123
1227
platform : List [str ],
@@ -1133,9 +1237,11 @@ def lock(
1133
1237
check_input_hash : bool ,
1134
1238
log_level : TLogLevel ,
1135
1239
pdb : bool ,
1136
- virtual_package_spec : Optional [PathLike ],
1240
+ virtual_package_spec : Optional [pathlib . Path ],
1137
1241
pypi_to_conda_lookup_file : Optional [str ],
1138
1242
update : Optional [List [str ]] = None ,
1243
+ metadata_choices : Sequence [str ] = (),
1244
+ metadata_yamls : Sequence [pathlib .Path ] = (),
1139
1245
) -> None :
1140
1246
"""Generate fully reproducible lock files for conda environments.
1141
1247
@@ -1155,6 +1261,10 @@ def lock(
1155
1261
if pypi_to_conda_lookup_file :
1156
1262
set_lookup_location (pypi_to_conda_lookup_file )
1157
1263
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
+
1158
1268
# bail out if we do not encounter the default file if no files were passed
1159
1269
if ctx .get_parameter_source ("files" ) == click .core .ParameterSource .DEFAULT :
1160
1270
candidates = list (files )
@@ -1199,6 +1309,8 @@ def lock(
1199
1309
virtual_package_spec = virtual_package_spec ,
1200
1310
update = update ,
1201
1311
filter_categories = filter_categories ,
1312
+ metadata_choices = metadata_enum_choices ,
1313
+ metadata_yamls = metadata_yamls ,
1202
1314
)
1203
1315
if strip_auth :
1204
1316
with tempfile .TemporaryDirectory () as tempdir :
0 commit comments