Skip to content

Commit bfbbb68

Browse files
authored
Merge pull request #61: Add config functions
2 parents b86c103 + 832b9c4 commit bfbbb68

File tree

2 files changed

+87
-3
lines changed

2 files changed

+87
-3
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ Potential Nextstrain CLI scripts
123123

124124
Snakemake workflow functions that are shared across many pathogen workflows that don’t really belong in any of our existing tools.
125125

126-
- [config.smk](snakemake/config.smk) - Shared functions for parsing workflow configs.
126+
- [config.smk](snakemake/config.smk) - Shared functions for handling workflow configs.
127127
- [remote_files.smk](snakemake/remote_files.smk) - Exposes the `path_or_url` function which will use Snakemake's storage plugins to download/upload files to remote providers as needed.
128128

129129

snakemake/config.smk

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
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
33
workflow configs.
44
"""
5-
import os.path
5+
import os
6+
import sys
7+
import yaml
68
from collections.abc import Callable
79
from snakemake.io import Wildcards
810
from 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+
1686
def 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

Comments
 (0)