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 method to write default config to Tool and Component. #2378

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
25 changes: 25 additions & 0 deletions ctapipe/core/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from traitlets import TraitError
from traitlets.config import Configurable

from . import config_writer
from .plugins import detect_and_import_plugins

__all__ = ["non_abstract_children", "Component"]
Expand Down Expand Up @@ -276,3 +277,27 @@
state["_trait_values"]["parent"] = None
state["_trait_notifiers"] = {}
return state

@classmethod
def _get_default_config(cls):
"""

:return:
"""
return config_writer.get_default_config(cls)

Check warning on line 287 in ctapipe/core/component.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/component.py#L287

Added line #L287 was not covered by tests

@classmethod
def write_default_config(cls, outname=None):
"""return the current configuration as a dict (e.g. the values
of all traits, even if they were not set during configuration)
"""

if outname is None:
outname = f"{cls.__name__}.yml"

Check warning on line 296 in ctapipe/core/component.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/component.py#L295-L296

Added lines #L295 - L296 were not covered by tests

conf = cls._get_default_config()

Check warning on line 298 in ctapipe/core/component.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/component.py#L298

Added line #L298 was not covered by tests

conf_repr = config_writer.trait_dict_to_yaml(conf)

Check warning on line 300 in ctapipe/core/component.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/component.py#L300

Added line #L300 was not covered by tests

with open(outname, "w") as obj:
obj.write(conf_repr)

Check warning on line 303 in ctapipe/core/component.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/component.py#L302-L303

Added lines #L302 - L303 were not covered by tests
240 changes: 240 additions & 0 deletions ctapipe/core/config_writer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import logging
import re
import textwrap

import traitlets

log = logging.getLogger(__name__)


def trait_dict_to_yaml(conf, conf_repr="", indent_level=0):
"""
Using a dictionnary of traits, will write this configuration to file. Each value is either a subsection or a
trait object so that we can extract value, default value and help message

:param conf: Dictionnary of traits. Architecture reflect what is needed in the yaml file.
:param str conf_repr: internal variable used for recursivity. You shouldn't use that parameter
:param int indent_level: internal variable used for recursivity. You shouldn't use that parameter

:return: str representation of conf, ready to store in a .yaml file
"""
indent_str = " "

Check warning on line 21 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L21

Added line #L21 was not covered by tests

for k, v in conf.items():
if isinstance(v, dict):

Check warning on line 24 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L23-L24

Added lines #L23 - L24 were not covered by tests
# Separate this new block from previous content
conf_repr += "\n"

Check warning on line 26 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L26

Added line #L26 was not covered by tests

# Add summary line from class docstring
class_help = v.pop(

Check warning on line 29 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L29

Added line #L29 was not covered by tests
"__doc__"
) # Pop to avoid treating this key:value as a parameter later on.
conf_repr += f"{wrap_comment(class_help, indent_level=indent_level)}\n"

Check warning on line 32 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L32

Added line #L32 was not covered by tests

conf_repr += f"{indent_str * indent_level}{k}:\n"
conf_repr = trait_dict_to_yaml(v, conf_repr, indent_level=indent_level + 1)

Check warning on line 35 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L34-L35

Added lines #L34 - L35 were not covered by tests
else:
conf_repr += _trait_to_str(v, indent_level=indent_level)

Check warning on line 37 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L37

Added line #L37 was not covered by tests

return conf_repr

Check warning on line 39 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L39

Added line #L39 was not covered by tests


def _trait_to_str(trait, help=True, indent_level=0):
"""
Represent a trait in a futur yaml file, given prior information on its position.

:param traitlets.trait trait:
:param bool help: [optional] True by default
:param indent_level: Indentation level to apply to the trait when creating the string, for correct display in
parent string.

:return: String representation of the input trait.
:rtype: str
"""
indent_str = " "

Check warning on line 54 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L54

Added line #L54 was not covered by tests

trait_repr = "\n"

Check warning on line 56 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L56

Added line #L56 was not covered by tests

trait_type = get_trait_type(trait)

Check warning on line 58 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L58

Added line #L58 was not covered by tests

# By default, help message only have info about parameter type
h_msg = f"[{trait_type}] "

Check warning on line 61 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L61

Added line #L61 was not covered by tests

if "Enum" in trait_type:
enum_values = get_enum_values(trait)
h_msg += f"(Possible values: {enum_values}) "

Check warning on line 65 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L63-L65

Added lines #L63 - L65 were not covered by tests

if help:
h_msg += trait.help

Check warning on line 68 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L67-L68

Added lines #L67 - L68 were not covered by tests

# Get rid of unnecessary formatting because we'll redo that
h_msg = clean_help_msg(h_msg)

Check warning on line 71 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L71

Added line #L71 was not covered by tests

trait_repr += f"{wrap_comment(h_msg, indent_level=indent_level)}\n"

Check warning on line 73 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L73

Added line #L73 was not covered by tests

trait_value = trait.default()

Check warning on line 75 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L75

Added line #L75 was not covered by tests

# add quotes for strings
if isinstance(trait, traitlets.Unicode):
trait_value = f"'{trait_value}'"

Check warning on line 79 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L78-L79

Added lines #L78 - L79 were not covered by tests

# Make sure that list of values end up on multiple lines (In particular quality criteria)
# Since traitlets.List derivative can't be compared with it, nor list/tuple, I have to check if "List" is in the
# type (converted as str)
value_repr = ""
trait_value_type = str(type(trait_value))
trait_value_is_list = ("List" in trait_value_type) or isinstance(trait_value, list)
if trait_value_is_list:
for v in trait_value:

Check warning on line 88 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L84-L88

Added lines #L84 - L88 were not covered by tests
# tuples are not correctly handled by yaml, so we convert them to list here. They'll be converted to tuple
# when reading again.
if isinstance(v, tuple):
v = list(v)
value_repr += f"\n{indent_str * indent_level}- {v}"

Check warning on line 93 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L91-L93

Added lines #L91 - L93 were not covered by tests
else:
value_repr += f"{trait_value}"

Check warning on line 95 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L95

Added line #L95 was not covered by tests

# Automatically comment all parameters that are unvalid
commented = ""
if trait_value in (traitlets.Undefined, None):
commented = "#"
elif trait_type.startswith("List") and len(trait_value) == 0:
commented = "#"

Check warning on line 102 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L98-L102

Added lines #L98 - L102 were not covered by tests

trait_repr += f"{indent_str*indent_level}{commented}{trait.name}: {value_repr}\n"

Check warning on line 104 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L104

Added line #L104 was not covered by tests

return trait_repr

Check warning on line 106 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L106

Added line #L106 was not covered by tests


def get_trait_type(trait):
"""
Get trait type. If needed, use recursion for sub-types in case of list, set...

:param traitlets.trait trait: Input trait

:return: str representation of the trait type
:rtype: str
"""
_repr = f"{trait.__class__.__name__}"

Check warning on line 118 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L118

Added line #L118 was not covered by tests

if hasattr(trait, "_trait"):
_repr += f"({get_trait_type(trait._trait)})"

Check warning on line 121 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L120-L121

Added lines #L120 - L121 were not covered by tests

return _repr

Check warning on line 123 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L123

Added line #L123 was not covered by tests


def get_enum_values(trait):
"""
Get possible values for a trait with Enum. Note that this can also be a list of enum.

We do not test the trait, we assume that 'trait' is either Enum or has a sub-trait with Enum in it.
If needed, use recursion for sub-types in case of list, set...

:param traitlets.trait trait: Input trait

:return: List of possible values for the Enum in this trait
:rtype: str
"""
trait_type = trait.__class__.__name__

Check warning on line 138 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L138

Added line #L138 was not covered by tests

if trait_type in ["Enum", "CaselessStrEnum"]:
values = list(

Check warning on line 141 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L140-L141

Added lines #L140 - L141 were not covered by tests
trait.values
) # Sometimes, the output is not a list (e.g. norm_cls)
elif trait_type == "UseEnum":
values = trait.enum_class._member_names_

Check warning on line 145 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L144-L145

Added lines #L144 - L145 were not covered by tests
else:
try:
values = get_enum_values(trait._trait)
except AttributeError:
log.error(f"Can't find an Enum type in trait '{trait.name}'")
raise

Check warning on line 151 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L147-L151

Added lines #L147 - L151 were not covered by tests

return values

Check warning on line 153 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L153

Added line #L153 was not covered by tests


def get_summary_doc(cls):
"""
Applied on a class object, will retrieve the first line of the docstring.

:param obj cls:

:return: Summary line from input docstring
:rtype: str
"""
first_line = cls.__doc__.split("\n\n")[0]

Check warning on line 165 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L165

Added line #L165 was not covered by tests

first_line = clean_help_msg(first_line)

Check warning on line 167 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L167

Added line #L167 was not covered by tests

return first_line

Check warning on line 169 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L169

Added line #L169 was not covered by tests


def clean_help_msg(msg):
"""
Clean and merge lines in a string to have only one line, get rid of tabulation and extra spaces.

:param str msg:
:return: cleaned string
"""
# Merge all lines, including tabulation if need be
msg = re.sub("\n *", " ", msg)

Check warning on line 180 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L180

Added line #L180 was not covered by tests

# clean extra spaces (regexp tend to leave a starting space because there's usually a newline at the start)
msg = msg.strip()

Check warning on line 183 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L183

Added line #L183 was not covered by tests

return msg

Check warning on line 185 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L185

Added line #L185 was not covered by tests


def get_default_config(cls):
"""
Get list of all traits from that class.

This is intented to be used as a class methods for all included class a Tool might have. Since the method is
always the same, for the sake of maintainability, We'll just use that function.

:param cls:
:return:
"""
conf = {cls.__name__: cls.traits(cls, config=True)}

Check warning on line 198 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L198

Added line #L198 was not covered by tests

# Add class doc for later use
conf[cls.__name__]["__doc__"] = get_summary_doc(cls)

Check warning on line 201 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L201

Added line #L201 was not covered by tests

return conf

Check warning on line 203 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L203

Added line #L203 was not covered by tests


def wrap_comment(text, indent_level=0, width=144, indent_str=" "):
"""return a commented, wrapped block."""
return textwrap.fill(

Check warning on line 208 in ctapipe/core/config_writer.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/config_writer.py#L208

Added line #L208 was not covered by tests
text,
width=width,
initial_indent=indent_str * indent_level + "# ",
subsequent_indent=indent_str * indent_level + "# ",
)


# def check_conf_repr(conf_repr):
# """
# Check the conf representation for empty sections that will crash on read
#
# e.g. section EventSource has no valid parameter in the default, and crash with the error:
# > ValueError: values whose keys begin with an uppercase char must be Config instances: 'EventSource', None
#
# :param str conf_repr: Config representation for Tool or Component
#
# :return: Cleaned configuration representation
# """
# in_lines = conf_repr.split("\n")
#
# work_lines = [l.strip().split("#")[0] for l in in_lines]
#
# section_idx = -1
# section_indent = -1
# inside_section
# for i in range(len(work_lines)):
# line = work_lines[i]
# section_line = line.strip()[0].isupper() # Bool
# if section_line:
# section_idx = i
# section_indent = len(line) - len(line.lstrip())
#
35 changes: 34 additions & 1 deletion ctapipe/core/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from traitlets.config import Application, Config, Configurable

from .. import __version__ as version
from . import Provenance
from . import Provenance, config_writer
from .component import Component
from .logging import ColoredFormatter, create_logging_config
from .traits import Bool, Dict, Enum, Path
Expand Down Expand Up @@ -485,6 +485,39 @@

return conf

@classmethod
def _get_default_config(cls):
"""

:return:
"""
conf = {cls.__name__: cls.traits(cls, config=True)}

Check warning on line 494 in ctapipe/core/tool.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/tool.py#L494

Added line #L494 was not covered by tests

# Add class doc for later use
conf[cls.__name__]["__doc__"] = config_writer.get_summary_doc(cls)

Check warning on line 497 in ctapipe/core/tool.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/tool.py#L497

Added line #L497 was not covered by tests

# Get default configuration for all sub-classes defined
for val in cls.classes:
conf[cls.__name__].update(val._get_default_config())

Check warning on line 501 in ctapipe/core/tool.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/tool.py#L500-L501

Added lines #L500 - L501 were not covered by tests

return conf

Check warning on line 503 in ctapipe/core/tool.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/tool.py#L503

Added line #L503 was not covered by tests

@classmethod
def write_default_config(cls, outname=None):
"""return the current configuration as a dict (e.g. the values
of all traits, even if they were not set during configuration)
"""

if outname is None:
outname = f"{cls.__name__}.yml"

Check warning on line 512 in ctapipe/core/tool.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/tool.py#L511-L512

Added lines #L511 - L512 were not covered by tests

conf = cls._get_default_config()

Check warning on line 514 in ctapipe/core/tool.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/tool.py#L514

Added line #L514 was not covered by tests

conf_repr = config_writer.trait_dict_to_yaml(conf)

Check warning on line 516 in ctapipe/core/tool.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/tool.py#L516

Added line #L516 was not covered by tests

with open(outname, "w") as obj:
obj.write(conf_repr)

Check warning on line 519 in ctapipe/core/tool.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/core/tool.py#L518-L519

Added lines #L518 - L519 were not covered by tests

def _repr_html_(self):
"""nice HTML rep, with blue for non-default values"""
traits = self.traits()
Expand Down
17 changes: 17 additions & 0 deletions ctapipe/io/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from traitlets import Enum, HasTraits, Instance, List, Unicode, UseEnum, default
from traitlets.config import Configurable

from ..core import config_writer
from ..core.traits import AstroTime
from .datalevels import DataLevel

Expand Down Expand Up @@ -93,6 +94,14 @@
def __repr__(self):
return f"{self.__class__.__name__}(name={self.name}, email={self.email}, organization={self.organization})"

@classmethod
def _get_default_config(cls):
"""

:return:
"""
return config_writer.get_default_config(cls)

Check warning on line 103 in ctapipe/io/metadata.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/io/metadata.py#L103

Added line #L103 was not covered by tests


class Product(HasTraits):
"""Data product information"""
Expand Down Expand Up @@ -218,6 +227,14 @@
")"
)

@classmethod
def _get_default_config(cls):
"""

:return:
"""
return config_writer.get_default_config(cls)

Check warning on line 236 in ctapipe/io/metadata.py

View check run for this annotation

Codecov / codecov/patch

ctapipe/io/metadata.py#L236

Added line #L236 was not covered by tests


def _to_dict(hastraits_instance, prefix=""):
"""helper to convert a HasTraits to a dict with keys
Expand Down
Loading