-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathconfig.smk
More file actions
156 lines (119 loc) · 5.59 KB
/
config.smk
File metadata and controls
156 lines (119 loc) · 5.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
"""
Shared functions to be used within a Snakemake workflow for handling
workflow configs.
"""
import os
import sys
import yaml
from collections.abc import Callable
from typing import Optional
from textwrap import dedent, indent
# Set search paths for Augur
if "AUGUR_SEARCH_PATHS" in os.environ:
print(dedent(f"""\
Using existing search paths in AUGUR_SEARCH_PATHS:
{os.environ["AUGUR_SEARCH_PATHS"]!r}
"""), file=sys.stderr)
else:
# Note that this differs from the search paths used in
# resolve_config_path().
# This is the preferred default moving forwards, and the plan is to
# eventually update resolve_config_path() to use AUGUR_SEARCH_PATHS.
search_paths = [
# User analysis directory
Path.cwd(),
# Workflow defaults folder
Path(workflow.basedir) / "defaults",
# Workflow root (contains Snakefile)
Path(workflow.basedir),
]
# This should work for majority of workflows, but we could consider doing a
# more thorough search for the nextstrain-pathogen.yaml. This would likely
# replicate how CLI searches for the root.¹
# ¹ <https://github.com/nextstrain/cli/blob/d5e184c5/nextstrain/cli/command/build.py#L413-L420>
repo_root = Path(workflow.basedir) / ".."
if (repo_root / "nextstrain-pathogen.yaml").is_file():
search_paths.extend([
# Pathogen repo root
repo_root,
])
search_paths = [path.resolve() for path in search_paths if path.is_dir()]
os.environ["AUGUR_SEARCH_PATHS"] = ":".join(map(str, search_paths))
class InvalidConfigError(Exception):
pass
def resolve_config_path(path: str, defaults_dir: Optional[str] = None) -> Callable:
"""
Resolve a relative *path* given in a configuration value. Will always try to
resolve *path* after expanding wildcards with Snakemake's `expand` functionality.
Returns the path for the first existing file, checked in the following order:
1. relative to the analysis directory or workdir, usually given by ``--directory`` (``-d``)
2. relative to *defaults_dir* if it's provided
3. relative to the workflow's ``defaults/`` directory if *defaults_dir* is _not_ provided
This behaviour allows a default configuration value to point to a default
auxiliary file while also letting the file used be overridden either by
setting an alternate file path in the configuration or by creating a file
with the conventional name in the workflow's analysis directory.
"""
global workflow
def _resolve_config_path(wildcards):
try:
expanded_path = expand(path, **wildcards)[0]
except snakemake.exceptions.WildcardError as e:
available_wildcards = "\n".join(f" - {wildcard}" for wildcard in wildcards)
raise snakemake.exceptions.WildcardError(indent(dedent(f"""\
{str(e)}
However, resolve_config_path({{path}}) requires the wildcard.
Wildcards available for this path are:
{{available_wildcards}}
Hint: Check that the config path value does not misspell the wildcard name
and that the rule actually uses the wildcard name.
""".lstrip("\n").rstrip()).format(path=repr(path), available_wildcards=available_wildcards), " " * 4))
if os.path.exists(expanded_path):
return expanded_path
if defaults_dir:
defaults_path = os.path.join(defaults_dir, expanded_path)
else:
# Special-case defaults/… for backwards compatibility with older
# configs. We could achieve the same behaviour with a symlink
# (defaults/defaults → .) but that seems less clear.
if path.startswith("defaults/"):
defaults_path = os.path.join(workflow.basedir, expanded_path)
else:
defaults_path = os.path.join(workflow.basedir, "defaults", expanded_path)
if os.path.exists(defaults_path):
return defaults_path
raise InvalidConfigError(indent(dedent(f"""\
Unable to resolve the config-provided path {path!r},
expanded to {expanded_path!r} after filling in wildcards.
The workflow does not include the default file {defaults_path!r}.
Hint: Check that the file {expanded_path!r} exists in your analysis
directory or remove the config param to use the workflow defaults.
"""), " " * 4))
return _resolve_config_path
def write_config(path, section=None):
"""
Write Snakemake's 'config' variable, or a section of it, to a file.
*section* is an optional list of keys to navigate to a specific section of
config. If provided, only that section will be written.
"""
global config
os.makedirs(os.path.dirname(path), exist_ok=True)
data = config
section_str = "config"
if section:
# Navigate to the specified section
for key in section:
# Error if key doesn't exist
if key not in data:
raise Exception(f"ERROR: Key {key!r} not found in {section_str!r}.")
data = data[key]
section_str += f".{key}"
# Error if value is not a mapping
if not isinstance(data, dict):
raise Exception(f"ERROR: {section_str!r} is not a mapping of key/value pairs.")
with open(path, 'w') as f:
yaml.dump(data, f, sort_keys=False, Dumper=NoAliasDumper)
print(f"Saved {section_str!r} to {path!r}.", file=sys.stderr)
class NoAliasDumper(yaml.SafeDumper):
def ignore_aliases(self, data):
return True