Skip to content

Settings

modelskill.settings

The settings module holds package-wide configurables and provides a uniform API for working with them.

This module is inspired by pandas config module.

Overview

This module supports the following requirements:

  • options are referenced using keys in dot.notation, e.g. "x.y.option - z".
  • keys are case-insensitive.
  • functions should accept partial/regex keys, when unambiguous.
  • options can be registered by modules at import time.
  • options have a default value, and (optionally) a description and validation function associated with them.
  • options can be reset to their default value.
  • all option can be reset to their default value at once.
  • all options in a certain sub - namespace can be reset at once.
  • the user can set / get / reset or ask for the description of an option.
  • a developer can register an option.
Implementation
  • Data is stored using nested dictionaries, and should be accessed through the provided API.
  • "Registered options" have metadata associated with them, which are stored in auxiliary dictionaries keyed on the fully-qualified key, e.g. "x.y.z.option".

Examples:

>>> import modelskill as ms
>>> ms.options
metrics.list : [<function bias at 0x0000029D614A2DD0>, (...)]
plot.rcparams : {}
plot.scatter.legend.bbox : {'facecolor': 'white', (...)}
plot.scatter.legend.fontsize : 12
plot.scatter.legend.kwargs : {}
plot.scatter.oneone_line.color : blue
plot.scatter.oneone_line.label : 1:1
plot.scatter.points.alpha : 0.5
plot.scatter.points.label :
plot.scatter.points.size : 20
plot.scatter.quantiles.color : darkturquoise
plot.scatter.quantiles.kwargs : {}
plot.scatter.quantiles.label : Q-Q
plot.scatter.quantiles.marker : X
plot.scatter.quantiles.markeredgecolor : (0, 0, 0, 0.4)
plot.scatter.quantiles.markeredgewidth : 0.5
plot.scatter.quantiles.markersize : 3.5
plot.scatter.reg_line.kwargs : {'color': 'r'}
>>> ms.set_option("plot.scatter.points.size", 4)
>>> plot.scatter.points.size
4
>>> ms.get_option("plot.scatter.points.size")
4
>>> ms.options.plot.scatter.points.size = 10
>>> ms.options.plot.scatter.points.size
10
>>> ms.reset_option("plot.scatter.points.size")
>>> ms.options.plot.scatter.points.size
20

OptionsContainer

provide attribute-style access to a nested dict of options

Accessed by ms.options

Source code in modelskill/settings.py
class OptionsContainer:
    """provide attribute-style access to a nested dict of options

    Accessed by ms.options
    """

    def __init__(self, d: Dict[str, Any], prefix: str = "") -> None:
        object.__setattr__(self, "d", d)
        object.__setattr__(self, "prefix", prefix)

    def __setattr__(self, key: str, val: Any) -> None:
        prefix = object.__getattribute__(self, "prefix")
        if prefix:
            prefix += "."
        prefix += key
        # you can't set new keys
        # can you can't overwrite subtrees
        if key in self.d and not isinstance(self.d[key], dict):
            set_option(prefix, val)
        else:
            raise OptionError("You can only set the value of existing options")

    def __getattr__(self, key: str):
        prefix = object.__getattribute__(self, "prefix")
        if prefix:
            prefix += "."
        prefix += key
        try:
            v = object.__getattribute__(self, "d")[key]
        except KeyError as err:
            raise OptionError(f"No such option: {key}") from err
        if isinstance(v, dict):
            return OptionsContainer(v, prefix)
        else:
            return get_option(prefix)

    def to_dict(self) -> Dict:
        """Return options as dictionary with full-name keys"""
        return _option_to_dict(self.prefix)

    # def search(self, pat: str = "") -> List[str]:
    #     keys = _select_options(f"{self.prefix}*{pat}")
    #     return list(keys)

    def __repr__(self) -> str:
        return _describe_option_short(self.prefix, False) or ""

    def __dir__(self) -> Iterable[str]:
        return list(self.d.keys())

to_dict

to_dict()

Return options as dictionary with full-name keys

Source code in modelskill/settings.py
def to_dict(self) -> Dict:
    """Return options as dictionary with full-name keys"""
    return _option_to_dict(self.prefix)

get_option

get_option(pat)

Get value of a single option matching a pattern

Parameters:

Name Type Description Default
pat str

pattern of seeked option

required

Returns:

Type Description
Any

value of matched option

Source code in modelskill/settings.py
def get_option(pat: str) -> Any:
    """Get value of a single option matching a pattern

    Parameters
    ----------
    pat : str
        pattern of seeked option

    Returns
    -------
    Any
        value of matched option
    """
    key = _get_single_key(pat)

    # walk the nested dict
    root, k = _get_root(key)
    return root[k]

load_style

load_style(name)

Load a number of options from a named style.

Parameters:

Name Type Description Default
name str

Name of the predefined style to load. Available styles are: 'MOOD': Resembling the plots of the www.metocean-on-demand.com data portal.

required

Raises:

Type Description
KeyError

If a named style is not found.

Examples:

>>> import modelskill as ms
>>> ms.load_style('MOOD')
Source code in modelskill/settings.py
def load_style(name: str) -> None:
    """Load a number of options from a named style.

    Parameters
    ----------
    name : str
        Name of the predefined style to load. Available styles are:
        'MOOD': Resembling the plots of the www.metocean-on-demand.com data portal.

    Raises
    ------
    KeyError
        If a named style is not found.

    Examples
    --------
    >>> import modelskill as ms
    >>> ms.load_style('MOOD')
    """

    lname = name.lower()

    # The number of folders to search can be expanded in the future
    path = Path(__file__).parent / "styles"
    NAMED_STYLES = {x.stem: x for x in path.glob("*.yml")}

    if lname not in NAMED_STYLES:
        raise KeyError(
            f"Style '{name}' not found. Choose from {list(NAMED_STYLES.keys())}"
        )

    style_path = NAMED_STYLES[lname]

    with open(style_path, encoding="utf-8") as f:
        contents = f.read()
        d = yaml.load(contents, Loader=yaml.FullLoader)

    set_option(d)

register_option

register_option(key, defval, doc='', validator=None)

Register an option in the package-wide modelskill settingss object

Parameters:

Name Type Description Default
key str

Fully-qualified key, e.g. "x.y.option - z".

required
defval object

Default value of the option.

required
doc str

Description of the option.

''
validator Callable

Function of a single argument, should raise ValueError if called with a value which is not a legal value for the option.

None

Raises:

Type Description
ValueError if `validator` is specified and `defval` is not a valid value.
Source code in modelskill/settings.py
def register_option(
    key: str,
    defval: object,
    doc: str = "",
    validator: Optional[Callable[[Any], Any]] = None,
    # cb: Optional[Callable[[str], Any]] = None,
) -> None:
    """
    Register an option in the package-wide modelskill settingss object

    Parameters
    ----------
    key : str
        Fully-qualified key, e.g. "x.y.option - z".
    defval : object
        Default value of the option.
    doc : str
        Description of the option.
    validator : Callable, optional
        Function of a single argument, should raise `ValueError` if
        called with a value which is not a legal value for the option.

    Raises
    ------
    ValueError if `validator` is specified and `defval` is not a valid value.
    """
    import keyword
    import tokenize

    key = key.lower()

    if key in _registered_options:
        raise OptionError(f"Option '{key}' has already been registered")
    # if key in _reserved_keys:
    #     raise OptionError(f"Option '{key}' is a reserved key")

    # the default value should be legal
    if validator:
        validator(defval)

    # walk the nested dict, creating dicts as needed along the path
    path = key.split(".")

    for k in path:
        if not re.match("^" + tokenize.Name + "$", k):
            raise ValueError(f"{k} is not a valid identifier")
        if keyword.iskeyword(k):
            raise ValueError(f"{k} is a python keyword")

    cursor = _global_settings
    msg = "Path prefix to option '{option}' is already an option"

    for i, p in enumerate(path[:-1]):
        if not isinstance(cursor, dict):
            raise OptionError(msg.format(option=".".join(path[:i])))
        if p not in cursor:
            cursor[p] = {}
        cursor = cursor[p]

    if not isinstance(cursor, dict):
        raise OptionError(msg.format(option=".".join(path[:-1])))

    cursor[path[-1]] = defval  # initialize

    # save the option metadata
    _registered_options[key] = RegisteredOption(
        key=key,
        defval=defval,
        doc=doc,
        validator=validator,  # , cb=cb
    )

reset_option

reset_option(pat='', silent=False)

Reset one or more options (matching a pattern) to the default value

Examples:

>>> ms.options.plot.scatter.points.size
20
>>> ms.options.plot.scatter.points.size = 10
>>> ms.options.plot.scatter.points.size
10
>>> ms.reset_option("plot.scatter.points.size")
>>> ms.options.plot.scatter.points.size
20
Source code in modelskill/settings.py
def reset_option(pat: str = "", silent: bool = False) -> None:
    """Reset one or more options (matching a pattern) to the default value

    Examples
    --------
    >>> ms.options.plot.scatter.points.size
    20
    >>> ms.options.plot.scatter.points.size = 10
    >>> ms.options.plot.scatter.points.size
    10
    >>> ms.reset_option("plot.scatter.points.size")
    >>> ms.options.plot.scatter.points.size
    20

    """

    keys = _select_options(pat)

    if len(keys) == 0:
        raise OptionError("No such keys(s)")

    if len(keys) > 1 and len(pat) < 4 and pat != "all":
        raise ValueError(
            "You must specify at least 4 characters when "
            "resetting multiple keys, use the special keyword "
            '"all" to reset all the options to their default value'
        )

    for k in keys:
        set_option(k, _registered_options[k].defval, silent=silent)

set_option

set_option(*args, **kwargs)

Set the value of one or more options

Examples:

>>> ms.set_option("plot.scatter.points.size", 4)
>>> ms.set_option({"plot.scatter.points.size": 4})
>>> ms.options.plot.scatter.points.size = 4
Source code in modelskill/settings.py
def set_option(*args, **kwargs) -> None:
    """Set the value of one or more options

    Examples
    --------
    >>> ms.set_option("plot.scatter.points.size", 4)
    >>> ms.set_option({"plot.scatter.points.size": 4})
    >>> ms.options.plot.scatter.points.size = 4
    """
    # must at least 1 arg deal with constraints later

    if len(args) == 1 and isinstance(args[0], dict):
        kwargs.update(args[0])

    if len(args) % 2 == 0:
        keys = args[::2]
        values = args[1::2]
        kwargs.update(dict(zip(keys, values)))

    if len(args) > 1 and len(args) % 2 != 0:
        raise ValueError("Must provide a value for each key, i.e. even number of args")

    # default to false
    kwargs.pop("silent", False)

    for k, v in kwargs.items():
        key = _get_single_key(k)  # , silent)

        o = _get_registered_option(key)
        if o and o.validator:
            o.validator(v)

        # walk the nested dict
        root, k = _get_root(key)
        root[k] = v