11"""
2- Shared functions to be used within a Snakemake workflow for parsing
2+ Shared functions to be used within a Snakemake workflow for handling
33workflow configs.
44"""
5- import os .path
5+ import os
6+ import sys
7+ import yaml
68from collections .abc import Callable
79from snakemake .io import Wildcards
810from typing import Optional
@@ -13,6 +15,74 @@ class InvalidConfigError(Exception):
1315 pass
1416
1517
18+ def resolve_filepaths (filepaths ):
19+ """
20+ Update filepaths in-place by passing them through resolve_config_path().
21+
22+ `filepaths` must be a list of key tuples representing filepaths in config.
23+ Use "*" as a a key-expansion placeholder: it means "iterate over all keys at
24+ this level".
25+ """
26+ global config
27+
28+ for keys in filepaths :
29+ _traverse (config , keys , traversed_keys = [])
30+
31+
32+ def _traverse (config_section , keys , traversed_keys ):
33+ """
34+ Recursively walk through the config following a list of keys.
35+
36+ When the final key is reached, the value is updated in place.
37+ """
38+ key = keys [0 ]
39+ remaining_keys = keys [1 :]
40+
41+ if key == "*" :
42+ for key in config_section :
43+ if len (remaining_keys ) == 0 :
44+ _update_value_inplace (config_section , key , traversed_keys = traversed_keys + [f'* ({ key } )' ])
45+ else :
46+ if isinstance (config_section [key ], dict ):
47+ _traverse (config_section [key ], remaining_keys , traversed_keys = traversed_keys + [f'* ({ key } )' ])
48+ else :
49+ # Value for key is not a dict
50+ # Leave as-is - this may be valid config value.
51+ continue
52+ elif key in config_section :
53+ if len (remaining_keys ) == 0 :
54+ _update_value_inplace (config_section , key , traversed_keys = traversed_keys + [key ])
55+ else :
56+ _traverse (config_section [key ], remaining_keys , traversed_keys = traversed_keys + [key ])
57+ else :
58+ # Key not present in config section
59+ # Ignore - this may be an optional parameter.
60+ return
61+
62+
63+ def _update_value_inplace (config_section , key , traversed_keys ):
64+ """
65+ Update the value at 'config_section[key]' with resolve_config_path().
66+
67+ resolve_config_path() returns a callable which has the ability to replace
68+ {var} in filepath strings. This was originally designed to support Snakemake
69+ wildcards, but those are not applicable here since this code is not running
70+ in the context of a Snakemake rule. It is unused here - the callable is
71+ given an empty dict.
72+ """
73+ value = config_section [key ]
74+ traversed = ' → ' .join (repr (key ) for key in traversed_keys )
75+ if isinstance (value , list ):
76+ for path in value :
77+ assert isinstance (path , str ), f"ERROR: Expected string but got { type (path ).__name__ } at { traversed } ."
78+ new_value = [resolve_config_path (path )({}) for path in value ]
79+ else :
80+ assert isinstance (value , str ), f"ERROR: Expected string but got { type (value ).__name__ } at { traversed } ."
81+ new_value = resolve_config_path (value )({})
82+ config_section [key ] = new_value
83+ print (f"Resolved { value !r} to { new_value !r} ." , file = sys .stderr )
84+
85+
1686def resolve_config_path (path : str , defaults_dir : Optional [str ] = None ) -> Callable [[Wildcards ], str ]:
1787 """
1888 Resolve a relative *path* given in a configuration value. Will always try to
@@ -75,3 +145,17 @@ def resolve_config_path(path: str, defaults_dir: Optional[str] = None) -> Callab
75145 """ ), " " * 4 ))
76146
77147 return _resolve_config_path
148+
149+
150+ def write_config (path ):
151+ """
152+ Write Snakemake's 'config' variable to a file.
153+ """
154+ global config
155+
156+ os .makedirs (os .path .dirname (path ), exist_ok = True )
157+
158+ with open (path , 'w' ) as f :
159+ yaml .dump (config , f , sort_keys = False )
160+
161+ print (f"Saved current run config to { path !r} ." , file = sys .stderr )
0 commit comments