"""Basic Option and Config functionality."""
import hashlib
import json
from abc import ABC
from .cache import Hashable
from .graph import Node
[docs]class Option(ABC):
"""An Option is a JSON serializable argument to a function."""
value_type = None
[docs] def __init__(self, desc=None, name=None, required=False, default=None):
"""Construct an 'Option' with name, default value, and description."""
self.name = name
self.prefix = ""
self.desc = desc
if default is not None and not isinstance(default, self.value_type):
raise ValueError(f"Default value {default} is not of type {self.value_type} or None")
self.default = default
self._required = required
# tracks whether value is set yet. Optional values are always set
self._missing = required
self._value = default
[docs] def add_argument(self, parser):
"""Add an argument to an argparse 'ArgumentParser'."""
parser.add_argument(
f"--{self.prefix}{self.name}",
default=self.default,
required=self._required,
type=self.value_type,
help=self.desc,
)
[docs] def get_value(self):
"""Get the set value or raise a ValueError."""
if self._missing:
raise ValueError(
"Required configuration option {self.prefix}{self.name} not set"
)
return self._value
[docs] def set_value(self, value):
"""Implement a guarded setter for this Option type."""
if not isinstance(value, self.value_type):
raise TypeError(
f"Cannot set option with type {str(self.value_type)} with "
f"value of type {str(type(value))}"
)
self._value = value
self._missing = False
value = property(get_value, set_value)
[docs] def __str__(self):
"""Show value and name for this Option."""
return (f"{self.__class__.__name__}(name={self.name}, "
f"_value={self._value})")
[docs]class Config(Hashable, Node):
"""A 'Config' is a collection of 'Option's."""
[docs] def __init__(self, options):
"""Construct a Config with given list of 'Option's."""
self._options = dict([(o.name, o) for o in options])
for opt in options:
if opt.name in self.__dict__:
raise ValueError(f"Option {opt.name} is already present in Config")
self.__dict__[opt.name] = opt
[docs] def __setattr__(self, name, val):
"""Overload setattr to only allow setting of listed options."""
if name == "_options":
object.__setattr__(self, name, val)
if name in self._options:
self._options[name].value = val
[docs] def __str__(self):
"""Convert to string by listing all options."""
return f"{self.__class__.__name__}: {{" + \
", ".join([n + "=" + str(o)
for n, o in self._options.items()]) + "}"
[docs] def to_dict(self):
"""Convert to dictionary to prepare for JSON conversion."""
return {n: o.value for n, o in self._options.items()}
[docs] def get_hash(self):
"""Return hash of the dictionary representation of this 'Config'."""
return hashlib.md5("conf" +
json.dumps(self.to_dict, sort_keys=True)).hexdigest()
[docs] def parents(self):
"""Return empty list of parents."""
return []
[docs]class Configurable(Node):
"""A Configurable class contains a 'Config' attribute called '.config'."""
OPTIONS = []
[docs] def __init__(self):
"""Set a default .config using the class attribute '.OPTIONS'."""
self.config = Config(self.OPTIONS)
[docs] def parents(self):
"""Return `.config` as the only parent."""
return [self.config]