Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a command print_named_configs #332

Merged
merged 3 commits into from
Jul 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion docs/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ The name of the command has to be first on the commandline::

>>> ./my_demo.py COMMAND_NAME with seed=123

If the COMMAND_NAME is ommitted it defaults to the main function, but the name
If the COMMAND_NAME is omitted it defaults to the main function, but the name
of that function can also explicitly used as the name of the command.
So for this experiment

Expand Down Expand Up @@ -263,6 +263,32 @@ The format for exporting the config is inferred from the filename and can be
any format supported for :ref:`config files <config_files>`.


.. _print_named_configs:

Print Named Configs
-------------------

The ``print_named_configs`` command prints all available named configurations.
Function docstrings for named config functions are copied and displayed colored
in **grey**.
For example::

>> ./named_config print_named_configs
INFO - hello_config - Running command 'print_named_configs'
INFO - hello_config - Started
Named Configurations (doc):
rude # A rude named config
INFO - hello_config - Completed after 0:00:00

If no named configs are available for the experiment, an empty list is printed::

>> ./01_hello_world print_named_configs
INFO - 01_hello_world - Running command 'print_named_configs'
INFO - 01_hello_world - Started
Named Configurations (doc):
No named configs
INFO - 01_hello_world - Completed after 0:00:00

Custom Commands
---------------
If you just run an experiment file it will execute the default command, that
Expand Down
1 change: 1 addition & 0 deletions examples/named_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

@ex.named_config
def rude():
"""A rude named config"""
recipient = "bastard"
message = "Fuck off you {}!".format(recipient)

Expand Down
47 changes: 45 additions & 2 deletions sacred/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
import pprint
import pydoc
import re
from collections import namedtuple
from collections import namedtuple, OrderedDict

from sacred.config import save_config_file
from sacred.serializer import flatten
from sacred.utils import PATHCHANGE, iterate_flattened_separately

__all__ = ('print_config', 'print_dependencies', 'save_config',
'help_for_command')
'help_for_command', 'print_named_configs')

BLUE = '\033[94m'
GREEN = '\033[92m'
Expand Down Expand Up @@ -62,6 +62,49 @@ def print_config(_run):
print(_format_config(final_config, config_mods))


def _format_named_config(indent, path, named_config):
indent = ' ' * indent
assign = path
if hasattr(named_config, '__doc__') and named_config.__doc__ is not None:
doc_string = named_config.__doc__
if doc_string.strip().count('\n') == 0:
assign += GREY + ' # {}'.format(doc_string.strip()) + ENDC
else:
doc_string = doc_string.replace('\n', '\n' + indent)
assign += GREY + '\n{}"""{}"""'.format(indent + ' ',
doc_string) + ENDC
return indent + assign


def _format_named_configs(named_configs, indent=2, hide_path=None):
lines = ['Named Configurations (' + GREY + 'doc' + ENDC + '):']
for path, named_config in named_configs.items():
if hide_path is not None and path.startswith(hide_path + '.'):
path = path[len(hide_path) + 1:]
lines.append(_format_named_config(indent, path, named_config))
if len(lines) < 2:
lines.append(' ' * indent + 'No named configs')
return '\n'.join(lines)


def print_named_configs(ingredient):
"""
Returns a command function that prints the available named configs for the
ingredient and all sub-ingredients and exits.

The output is highlighted:
white: config names
grey: doc
"""

def print_named_configs():
"""Print the available named configs and exit."""
named_configs = OrderedDict(ingredient.gather_named_configs())
print(_format_named_configs(named_configs, 2, ingredient.path))

return print_named_configs


def help_for_command(command):
"""Get the help text (signature + docstring) for a command (function)."""
help_text = pydoc.text.document(command)
Expand Down
1 change: 1 addition & 0 deletions sacred/config/config_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def __init__(self, func):
self._func = func
self._body_code = get_function_body_code(func)
self._var_docs = get_config_comments(func)
self.__doc__ = self._func.__doc__

def __call__(self, fixed=None, preset=None, fallback=None):
"""
Expand Down
4 changes: 3 additions & 1 deletion sacred/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
from sacred.commandline_options import (
ForceOption, gather_command_line_options, LoglevelOption)
from sacred.commands import (help_for_command, print_config,
print_dependencies, save_config)
print_dependencies, save_config,
print_named_configs)
from sacred.config.signature import Signature
from sacred.ingredient import Ingredient
from sacred.initialize import create_run
Expand Down Expand Up @@ -82,6 +83,7 @@ def __init__(self, name=None, ingredients=(), interactive=False,
self.command(print_config, unobserved=True)
self.command(print_dependencies, unobserved=True)
self.command(save_config, unobserved=True)
self.command(print_named_configs(self), unobserved=True)
self.observers = []
self.current_run = None
self.captured_out_filter = None
Expand Down
17 changes: 17 additions & 0 deletions sacred/ingredient.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,23 @@ def gather_commands(self):
for cmd_name, cmd in ingred.gather_commands():
yield cmd_name, cmd

def gather_named_configs(self):
"""Collect all named configs from this ingredient and its sub-ingredients.

Yields
------
config_name: str
The full (dotted) name of the named config.
config: ConfigScope or ConfigDict or basestring
The corresponding named config.
"""
for config_name, config in self.named_configs.items():
yield self.path + '.' + config_name, config

for ingred in self.ingredients:
for config_name, config in ingred.gather_named_configs():
yield config_name, config

def get_experiment_info(self):
"""Get a dictionary with information about this experiment.

Expand Down
57 changes: 56 additions & 1 deletion tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
from __future__ import division, print_function, unicode_literals

import pprint
from collections import OrderedDict

import pytest
from sacred import Ingredient, Experiment
from sacred.commands import (BLUE, ENDC, GREY, GREEN, RED, ConfigEntry,
PathEntry, _format_config, _format_entry,
help_for_command, _iterate_marked, _non_unicode_repr)
help_for_command, _iterate_marked, _non_unicode_repr,
_format_named_configs, _format_named_config)
from sacred.config import ConfigScope
from sacred.config.config_summary import ConfigSummary


Expand Down Expand Up @@ -161,3 +165,54 @@ def my_command():
help_text = help_for_command(my_command)
assert "my_command" in help_text
assert "This is my docstring" in help_text


def _config_scope_with_single_line_doc():
"""doc"""
pass


def _config_scope_with_multiline_doc():
"""Multiline
docstring!
"""
pass


@pytest.mark.parametrize('indent, path, named_config, expected', [
(0, 'a', None, 'a'),
(1, 'b', None, ' b'),
(4, 'a.b.c', None, ' a.b.c'),
(0, 'c', ConfigScope(_config_scope_with_single_line_doc), 'c' + GREY
+ ' # doc' + ENDC),
(0, 'd', ConfigScope(_config_scope_with_multiline_doc),
'd' + GREY + '\n """Multiline\n docstring!\n """' + ENDC)
])
def test_format_named_config(indent, path, named_config, expected):
assert _format_named_config(indent, path, named_config) == expected


def test_format_named_configs():
ingred = Ingredient('ingred')
ex = Experiment(name='experiment', ingredients=[ingred])

@ingred.named_config
def named_config1():
pass

@ex.named_config
def named_config2():
"""named config with doc"""
pass

dict_config = dict(v=42)
ingred.add_named_config('dict_config', dict_config)

named_configs_text = _format_named_configs(OrderedDict(
ex.gather_named_configs()), hide_path=ex.path)
assert named_configs_text.startswith('Named Configurations (' +
GREY + 'doc' + ENDC + '):')
assert 'named_config2' in named_configs_text
assert '# named config with doc' in named_configs_text
assert 'ingred.named_config1' in named_configs_text
assert 'ingred.dict_config' in named_configs_text
17 changes: 17 additions & 0 deletions tests/test_ingredients.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,23 @@ def bar():
assert ('tickle.foo', foo) in commands


def test_gather_named_configs(ing):
ing2 = Ingredient('ing2', ingredients=[ing])

@ing.named_config
def named_config1():
pass

@ing2.named_config
def named_config2():
"""named config with doc"""
pass

named_configs = list(ing2.gather_named_configs())
assert ('ing2.named_config2', named_config2) in named_configs
assert ('tickle.named_config1', named_config1) in named_configs


def test_config_docs_are_preserved(ing):
@ing.config
def ing_cfg():
Expand Down