Source code for lightwin.config.key_val_conf_spec

"""Define the base objects constraining values/types of config parameters."""

import logging
from collections.abc import Collection
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal

from lightwin.config.csv_formatter import format_long_columns
from lightwin.config.helper import find_path
from lightwin.config.toml_formatter import format_for_toml

CSV_HEADER = ["Entry", "Type", "Description", "Mandatory?", "Allowed values"]
CSV_WIDTHS = (20, 10, 30, 1000, 1000)


[docs] @dataclass class KeyValConfSpec: """Set specifications for a single key-value pair. Parameters ---------- key : Name of the attribute. types : Allowed types for the value. Used to check validity of input. When creating a config ``TOML`` file, the first type of the tuple is used for proper formatting. Prefer giving a tuple of types, even if there is only one possible type. description : A markdown string to describe the property. Will be displayed in the documentation. default_value : A default value for the property. Used when generating dummy configurations; also used if the property is not mandatory and was not provided. allowed_values : A set of allowed values, or range of allowed values. The default is None, in which case no checking is performed. is_mandatory : If the property must be given. is_a_path_that_must_exists : If the property is a string/path and its existence must be checked before running the code. action : on/off flag, also check the ``argparse`` documentation. Will skip testing over type and allowed values. warning_message : If provided, using current key will print a warning with this message. error_message : If provided, using current key will raise an IOError with this error message. overrides_previously_defined : If the current object should remove a previously defined :class:`KeyValConfSpec` with the same name. derived : If the property is calculated from other properties. The default is False, in which case it must be set by the user. Note that derived keys will not appear in the ``TOML`` output strings. """ key: str types: tuple[type, ...] description: str default_value: Any allowed_values: Collection[Any] | None = None is_mandatory: bool = True is_a_path_that_must_exists: bool = False action: Literal["store_true", "store_false"] | None = None warning_message: str | None = None error_message: str | None = None overrides_previously_defined: bool = False derived: bool = False
[docs] def __post_init__(self) -> None: """Force ``self.types`` to be a tuple of types.""" if isinstance(self.types, type): self.types = (self.types,)
[docs] def validate(self, toml_value: Any, **kwargs) -> bool: """Check that the given ``toml`` line is valid.""" if self.warning_message: logging.warning(f"{self.key}: {self.warning_message}") if self.error_message: logging.critical(f"{self.key}: {self.error_message}") raise OSError(f"{self.key}: {self.error_message}") if self.action is not None: return True valid = ( self.is_valid_type(toml_value, **kwargs) and self.is_valid_value(toml_value, **kwargs) and self.path_exists(toml_value, **kwargs) ) if not valid: logging.error(f"An error was detected while treating {self.key}") return valid
[docs] def is_valid_type(self, toml_value: Any, **kwargs) -> bool: """Check that the value has the proper typing.""" if isinstance(toml_value, self.types): return True logging.warning( f"Type error in {self.key}. {toml_value = } type not in {self.types = }" ) return False
[docs] def is_valid_value(self, toml_value: Any, **kwargs) -> bool: """Check that the value is accepted.""" if self.allowed_values is None: return True if toml_value in self.allowed_values: return True logging.error( f"{self.key}: {toml_value = } is not in {self.allowed_values = }" ) return False
[docs] def path_exists( self, toml_value: Any, toml_folder: Path | None = None, **kwargs ) -> bool: """Check that the given path exists if necessary.""" if not self.is_a_path_that_must_exists: return True try: _ = find_path(toml_folder, toml_value) return True except FileNotFoundError: logging.error(f"{toml_value} should exist but was not found.") return False
[docs] def to_toml_string( self, toml_value: Any | None = None, original_toml_folder: Path | None = None, **kwargs, ) -> str: """Convert the value into a line that can be put in a ``TOML``. Parameters ---------- toml_value : The value to put in the file. If not provided, we issue a warning and set at default value. original_toml_folder : Where the original ``TOML`` was; this is used to resolve paths relative to this location. Returns ------- The ``TOML`` line corresponding to current object. """ if self.derived: return "" if toml_value is None: logging.error( f"You must provide a value for {self.key = }. Trying to " f"continue with {self.default_value = }..." ) toml_value = self.default_value if Path in self.types: assert isinstance(toml_value, (str, Path)) toml_value = find_path(original_toml_folder, toml_value) formatted = format_for_toml( self.key, toml_value, preferred_type=self.types[0] ) return formatted
[docs] def to_csv_line(self) -> list[str] | None: """Convert object to a line for the documentation ``CSV``. .. todo:: Better display of allowed values Returns ------- key : Name of variable. types : list of allowed types. description : Description of the input. allowed_values : list of allowed values if relatable. is_mandatory : If the variable is mandatory or not. """ if self.derived: return None type_names = [f"`{t.__name__}`" for t in self.types] fmt_types = " or ".join(type_names) fmt_mandatory = "✅" if self.is_mandatory else "❌" fmt_allowed = ( f"{self.allowed_values}" if self.allowed_values is not None else "" ) long = ( f"`{self.key}`", fmt_types, self.description, fmt_mandatory, fmt_allowed, ) shortened = [ format_long_columns(text, width) for text, width in zip(long, CSV_WIDTHS) ] return shortened