Coverage for sacred/sacred/commands.py: 30%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
2# coding=utf-8
3"""Defines the stock-commands that every sacred experiment ships with."""
4import copy
6import pprint
7import pydoc
8import re
9import sys
10from collections import namedtuple, OrderedDict
12from colorama import Fore, Style
14from sacred.config import save_config_file
15from sacred.serializer import flatten
16from sacred.utils import PATHCHANGE, iterate_flattened_separately
18__all__ = (
19 "print_config",
20 "print_dependencies",
21 "save_config",
22 "help_for_command",
23 "print_named_configs",
24)
26COLOR_DIRTY = Fore.RED
27COLOR_TYPECHANGED = Fore.RED # prepend Style.BRIGHT for bold
28COLOR_ADDED = Fore.GREEN
29COLOR_MODIFIED = Fore.BLUE
30COLOR_DOC = Style.DIM
31ENDC = Style.RESET_ALL # '\033[0m'
33LEGEND = (
34 "("
35 + COLOR_MODIFIED
36 + "modified"
37 + ENDC
38 + ", "
39 + COLOR_ADDED
40 + "added"
41 + ENDC
42 + ", "
43 + COLOR_TYPECHANGED
44 + "typechanged"
45 + ENDC
46 + ", "
47 + COLOR_DOC
48 + "doc"
49 + ENDC
50 + ")"
51)
53ConfigEntry = namedtuple("ConfigEntry", "key value added modified typechanged doc")
54PathEntry = namedtuple("PathEntry", "key added modified typechanged doc")
57def _non_unicode_repr(objekt, context, maxlevels, level):
58 """
59 Used to override the pprint format method to get rid of unicode prefixes.
61 E.g.: 'John' instead of u'John'.
62 """
63 if sys.version_info[0] == 3 and sys.version_info[1] >= 8:
64 repr_string, isreadable, isrecursive = pprint._safe_repr(
65 objekt, context, maxlevels, level, sort_dicts=None
66 )
67 else:
68 repr_string, isreadable, isrecursive = pprint._safe_repr(
69 objekt, context, maxlevels, level
70 )
71 if repr_string.startswith('u"') or repr_string.startswith("u'"):
72 repr_string = repr_string[1:]
73 return repr_string, isreadable, isrecursive
76PRINTER = pprint.PrettyPrinter()
77PRINTER.format = _non_unicode_repr
80def print_config(_run):
81 """
82 Print the updated configuration and exit.
84 Text is highlighted:
85 green: value modified
86 blue: value added
87 red: value modified but type changed
88 """
89 final_config = _run.config
90 config_mods = _run.config_modifications
91 print(_format_config(final_config, config_mods))
94def _format_named_config(indent, path, named_config):
95 indent = " " * indent
96 assign = path
97 if hasattr(named_config, "__doc__") and named_config.__doc__ is not None:
98 doc_string = named_config.__doc__
99 if doc_string.strip().count("\n") == 0:
100 assign += COLOR_DOC + " # {}".format(doc_string.strip()) + ENDC
101 else:
102 doc_string = doc_string.replace("\n", "\n" + indent)
103 assign += (
104 COLOR_DOC + '\n{}"""{}"""'.format(indent + " ", doc_string) + ENDC
105 )
106 return indent + assign
109def _format_named_configs(named_configs, indent=2):
110 lines = ["Named Configurations (" + COLOR_DOC + "doc" + ENDC + "):"]
111 for path, named_config in named_configs.items():
112 lines.append(_format_named_config(indent, path, named_config))
113 if len(lines) < 2:
114 lines.append(" " * indent + "No named configs")
115 return "\n".join(lines)
118def print_named_configs(ingredient): # noqa: D202
119 """Returns a command that prints named configs recursively.
121 The command function prints the available named configs for the
122 ingredient and all sub-ingredients and exits.
124 Example
125 -------
126 The output is highlighted:
127 white: config names
128 grey: doc
129 """
131 def print_named_configs():
132 """Print the available named configs and exit."""
133 named_configs = OrderedDict(ingredient.gather_named_configs())
134 print(_format_named_configs(named_configs, 2))
136 return print_named_configs
139def help_for_command(command):
140 """Get the help text (signature + docstring) for a command (function)."""
141 help_text = pydoc.text.document(command)
142 # remove backspaces
143 return re.subn(".\\x08", "", help_text)[0]
146def print_dependencies(_run):
147 """Print the detected source-files and dependencies."""
148 print("Dependencies:")
149 for dep in _run.experiment_info["dependencies"]:
150 pack, _, version = dep.partition("==")
151 print(" {:<20} == {}".format(pack, version))
153 print("\nSources:")
154 for source, digest in _run.experiment_info["sources"]:
155 print(" {:<43} {}".format(source, digest))
157 if _run.experiment_info["repositories"]:
158 repos = _run.experiment_info["repositories"]
159 print("\nVersion Control:")
160 for repo in repos:
161 mod = COLOR_DIRTY + "M" if repo["dirty"] else " "
162 print("{} {:<43} {}".format(mod, repo["url"], repo["commit"]) + ENDC)
163 print("")
166def save_config(_config, _log, config_filename="config.json"):
167 """
168 Store the updated configuration in a file.
170 By default uses the filename "config.json", but that can be changed by
171 setting the config_filename config entry.
172 """
173 # Copy the config to make it mutable
174 _config = copy.deepcopy(_config)
175 if "config_filename" in _config:
176 del _config["config_filename"]
177 _log.info('Saving config to "{}"'.format(config_filename))
178 save_config_file(flatten(_config), config_filename)
181def _iterate_marked(cfg, config_mods):
182 for path, value in iterate_flattened_separately(cfg, ["__doc__"]):
183 if value is PATHCHANGE:
184 yield path, PathEntry(
185 key=path.rpartition(".")[2],
186 added=path in config_mods.added,
187 modified=path in config_mods.modified,
188 typechanged=config_mods.typechanged.get(path),
189 doc=config_mods.docs.get(path),
190 )
191 else:
192 yield path, ConfigEntry(
193 key=path.rpartition(".")[2],
194 value=value,
195 added=path in config_mods.added,
196 modified=path in config_mods.modified,
197 typechanged=config_mods.typechanged.get(path),
198 doc=config_mods.docs.get(path),
199 )
202def _format_entry(indent, entry):
203 color = ""
204 indent = " " * indent
205 if entry.typechanged:
206 color = COLOR_TYPECHANGED # red
207 elif entry.added:
208 color = COLOR_ADDED # green
209 elif entry.modified:
210 color = COLOR_MODIFIED # blue
211 if entry.key == "__doc__":
212 color = COLOR_DOC # grey
213 doc_string = entry.value.replace("\n", "\n" + indent)
214 assign = '{}"""{}"""'.format(indent, doc_string)
215 elif isinstance(entry, ConfigEntry):
216 assign = indent + entry.key + " = " + PRINTER.pformat(entry.value)
217 else: # isinstance(entry, PathEntry):
218 assign = indent + entry.key + ":"
219 if entry.doc:
220 doc_string = COLOR_DOC + "# " + entry.doc + ENDC
221 if len(assign) <= 35:
222 assign = "{:<35} {}".format(assign, doc_string)
223 else:
224 assign += " " + doc_string
225 end = ENDC if color else ""
226 return color + assign + end
229def _format_config(cfg, config_mods):
230 lines = ["Configuration " + LEGEND + ":"]
231 for path, entry in _iterate_marked(cfg, config_mods):
232 indent = 2 + 2 * path.count(".")
233 lines.append(_format_entry(indent, entry))
234 return "\n".join(lines)