""" Class to handle configuration for algorithms """
import warnings
import weakref
from abc import ABCMeta
from inspect import cleandoc, isabstract
from logging import getLogger
from docutils.core import publish_parts
from traitlets import TraitError
from traitlets.config import Configurable
from .plugins import detect_and_import_plugins
__all__ = ["non_abstract_children", "Component"]
def find_config_in_hierarchy(parent, class_name, trait_name):
"""
Find the value of a config item in the hierarchy by going up the hierarchy
from the parent and then down again to the child.
This is needed as parent.config is the full config and not the
config starting at the level of the parent.
"""
config = parent.config
# find the path from the config root to the desired object
hierarchy = [class_name]
while parent is not None:
hierarchy.append(parent.__class__.__name__)
parent = parent.parent
hierarchy = list(reversed(hierarchy))
# go down to the config value searched
# root key is optional
root = hierarchy.pop(0)
if root in config:
subconfig = config[root]
else:
subconfig = config
for name in hierarchy:
subconfig = subconfig[name]
return subconfig[trait_name]
[docs]def non_abstract_children(base):
"""
Return all non-abstract subclasses of a base class recursively.
Parameters
----------
base : class
High level class object that is inherited by the
desired subclasses
Returns
-------
non_abstract : dict
dict of all non-abstract subclasses
"""
subclasses = base.__subclasses__() + [
g for s in base.__subclasses__() for g in non_abstract_children(s)
]
non_abstract = [g for g in subclasses if not isabstract(g)]
return non_abstract
class AbstractConfigurableMeta(type(Configurable), ABCMeta):
"""
Metaclass to be able to make Component abstract
see: https://stackoverflow.com/a/7314847/3838691
"""
pass
[docs]class Component(Configurable, metaclass=AbstractConfigurableMeta):
"""Base class of all Components.
Components are classes that are configurable via traitlets
and setup a logger in the ctapipe logging hierarchy.
``traitlets`` can validate values and provide defaults and
descriptions. These will be automatically translated into
configuration parameters (command-line, config file, etc). Note
that any parameter that should be externally configurable must
have its ``config`` attribute set to ``True``, e.g. defined like
``myparam = Integer(0, help='the parameter').tag(config=True)``.
All components also contain a ``Logger`` instance in their ``log``
attribute, that you must use to output info, debugging data,
warnings, etc (do not use ``print()`` statements, instead use
``self.log.info()``, ``self.log.warning()``, ``self.log.debug()``, etc).
Components are generally used within `ctapipe.core.Tool`
subclasses, which provide configuration handling and command-line
tool generation.
For example:
.. code:: python
from ctapipe.core import Component
from traitlets import (Integer, Float)
class MyComponent(Component):
\"\"\" Does something \"\"\"
some_option = Integer(default_value=6,
help='a value to set').tag(config=True)
comp = MyComponent()
comp.some_option = 6 # ok
comp.some_option = 'test' # will fail validation
"""
def __init__(self, config=None, parent=None, **kwargs):
"""
Parameters
----------
config : traitlets.loader.Config
Configuration specified by config file or cmdline arguments.
Used to set traitlet values.
parent: Tool or Component
If a Component is created by another Component or Tool,
you need to pass the creating Component as parent, e.g.
`parent=self`. This makes sure the config is correctly
handed down to the child components.
Do not pass config in this case.
kwargs
Traitlets to be overridden.
TraitError is raised if kwargs contains a key that does not
correspond to a traitlet.
"""
if parent is not None and config is not None:
raise ValueError(
"Only one of `config` or `parent` allowed"
" If you create a Component as part of another, give `parent=self`"
" and not `config`"
)
# set up logging (for some reason the logger registered by LoggingConfig
# doesn't use a child logger of the parent by default)
if parent is not None:
self.log = parent.log.getChild(self.__class__.__name__)
else:
self.log = getLogger(
self.__class__.__module__ + "." + self.__class__.__name__
)
# Transform warning about wrong traitlets in the config to an error
# Only works for Components, unfortunately not for Tools, since
# Tools use `log.warning` instead of `warnings.warn`
with warnings.catch_warnings():
warnings.filterwarnings("error", message=".*Config option.*not recognized")
try:
if parent is not None:
parent = weakref.proxy(parent)
super().__init__(parent=parent, config=config, **kwargs)
except UserWarning as e:
raise TraitError(e) from None
for key in kwargs:
if not self.has_trait(key):
raise TraitError(f"Traitlet does not exist: {key}")
[docs] @classmethod
def from_name(cls, name, config=None, parent=None, **kwargs):
"""
Obtain an instance of a subclass via its name
Parameters
----------
name : str
Name of the subclass to obtain
config : traitlets.loader.Config
Configuration specified by config file or cmdline arguments.
Used to set traitlet values.
This argument is typically only specified when using this method
from within a Tool.
parent : ctapipe.core.Tool
Tool executable that is calling this component.
Passes the correct logger and configuration to the component.
This argument is typically only specified when using this method
from within a Tool (config need not be passed if parent is used).
kwargs
Returns
-------
instace
Instance of subclass to this class
"""
requested_subclass = cls.non_abstract_subclasses()[name]
return requested_subclass(config=config, parent=parent, **kwargs)
[docs] @classmethod
def non_abstract_subclasses(cls):
"""
get dict{name: cls} of non abstract subclasses,
subclasses can possibly be definded in plugins
"""
if hasattr(cls, "plugin_entry_point"):
detect_and_import_plugins(cls.plugin_entry_point)
subclasses = {base.__name__: base for base in non_abstract_children(cls)}
return subclasses
[docs] def get_current_config(self):
"""return the current configuration as a dict (e.g. the values
of all traits, even if they were not set during configuration)
"""
name = self.__class__.__name__
config = {name: {k: v.get(self) for k, v in self.traits(config=True).items()}}
for val in self.__dict__.values():
if isinstance(val, Component):
config[name].update(val.get_current_config())
return config
def _repr_html_(self):
"""nice HTML rep, with blue for non-default values"""
traits = self.traits()
name = self.__class__.__name__
docstring = (
publish_parts(cleandoc(self.__class__.__doc__), writer_name="html")[
"html_body"
]
or "Undocumented"
)
lines = [
"<div style='border:1px solid black; max-width: 700px; padding:2em'; word-wrap:break-word;>",
f"<b>{name}</b>",
f"<p> {docstring} </p>",
"<table>",
" <colgroup>",
" <col span='1' style=' '>",
" <col span='1' style='width: 20em;'>",
" <col span='1' >",
" </colgroup>",
" <tbody>",
]
for key, val in self.get_current_config()[name].items():
htmlval = (
str(val).replace("/", "/<wbr>").replace("_", "_<wbr>")
) # allow breaking at boundary
# traits of the current component
if key in traits:
thehelp = f"{traits[key].help} (default: {traits[key].default_value})"
lines.append(f"<tr><th>{key}</th>")
if val != traits[key].default_value:
lines.append(
f"<td style='text-align: left;'><span style='color:blue; max-width:30em;'>{htmlval}</span></td>"
)
else:
lines.append(f"<td style='text-align: left;'>{htmlval}</td>")
lines.append(
f"<td style='text-align: left;'><i>{thehelp}</i></td></tr>"
)
lines.append(" </tbody>")
lines.append("</table>")
lines.append("</div>")
return "\n".join(lines)
def __getstate__(self):
"""Make Components pickle-able by removing non-pickleable members"""
state = self.__dict__.copy()
state["_trait_values"]["parent"] = None
state["_trait_notifiers"] = {}
return state