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.helper import find_path
from lightwin.config.toml_formatter import format_for_toml


[docs] @dataclass class KeyValConfSpec: """Set specifications for a single key-value pair. Parameters ---------- key : str Name of the attribute. types : tuple[type, ...] 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 : str A markdown string to describe the property. Will be displayed in the documentation. default_value : Any 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 : Collection[Any] | None, optional A set of allowed values, or range of allowed values. The default is None, in which case no checking is performed. is_mandatory : bool, optional If the property must be given. The default is True. is_a_path_that_must_exists : bool, optional If the property is a string/path and its existence must be checked before running the code. action : Literal["store_true", "store_false"] | None = None on/off flag, also check the ``argparse`` documentation. Will skip testing over type and allowed values. warning_message : str | None, optional If provided, using current key will print a warning with this message. The default is None. error_message : str | None, optional If provided, using current key will raise an IOError with this error message. The default is None. overrides_previously_defined : bool, optional If the current object should remove a previously defined :class:`KeyValConfSpec` with the same name. derived : bool, optional 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(self.warning_message) if self.error_message: logging.critical(self.error_message) raise OSError(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 " f"{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 : Any | None, optional The value to put in the file. If not provided, we issue a warnign and set at default value. original_toml_folder : pathlib.Path | None, optional Where the original ``.toml`` was; this is used to resolve paths relative to this location. Returns ------- str 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) -> tuple[str, str, str, str, str] | None: """Convert object to a line for the documentation CSV. Returns ------- key : str Name of variable. types : str List of allowed types. description : str Description of the input. allowed_values : str List of allowed values if relatable. is_mandatory : str 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_allowed = ( f"{self.allowed_values}" if self.allowed_values is not None else "" ) fmt_mandatory = "✅" if self.is_mandatory else "❌" out = ( self.key, fmt_types, self.description, fmt_allowed, fmt_mandatory, ) return out