confkit
Module that provides the main interface for the confkit package.
It includes the Config class and various data types used for configuration values.
1"""Module that provides the main interface for the confkit package. 2 3It includes the Config class and various data types used for configuration values. 4""" 5from __future__ import annotations 6 7from .config import Config, ConfigContainerMeta 8from .data_types import ( 9 BaseDataType, 10 Binary, 11 Boolean, 12 Date, 13 DateTime, 14 Dict, 15 Enum, 16 Float, 17 Hex, 18 Integer, 19 IntEnum, 20 IntFlag, 21 List, 22 NoneType, 23 Octal, 24 Optional, 25 Set, 26 StrEnum, 27 String, 28 Time, 29 TimeDelta, 30 Tuple, 31) 32from .exceptions import ConfigPathConflictError, InvalidConverterError, InvalidDefaultError 33 34__all__ = [ 35 "BaseDataType", 36 "Binary", 37 "Boolean", 38 "Config", 39 "ConfigContainerMeta", 40 "ConfigPathConflictError", 41 "Date", 42 "DateTime", 43 "Dict", 44 "Enum", 45 "Float", 46 "Hex", 47 "IntEnum", 48 "IntFlag", 49 "Integer", 50 "InvalidConverterError", 51 "InvalidDefaultError", 52 "List", 53 "NoneType", 54 "Octal", 55 "Optional", 56 "Set", 57 "StrEnum", 58 "String", 59 "Time", 60 "TimeDelta", 61 "Tuple", 62]
23class BaseDataType(ABC, Generic[T]): 24 """Base class used for Config descriptors to define a data type.""" 25 26 def __init__(self, default: T) -> None: 27 """Initialize the base data type.""" 28 self.default = default 29 self.value = default 30 self.type = type(default) 31 32 def __str__(self) -> str: 33 """Return the string representation of the stored value.""" 34 return str(self.value) 35 36 @abstractmethod 37 def convert(self, value: str) -> T: 38 """Convert a string value to the desired type.""" 39 40 def validate(self) -> bool: 41 """Validate that the value matches the expected type.""" 42 orig_bases: tuple[type, ...] | None = getattr(self.__class__, "__orig_bases__", None) 43 44 if not orig_bases: 45 msg = "No type information available for validation." 46 raise InvalidConverterError(msg) 47 48 # Extract type arguments from the generic base 49 for base in orig_bases: 50 if hasattr(base, "__args__"): 51 type_args = base.__args__ 52 if type_args: 53 for type_arg in type_args: 54 if hasattr(type_arg, "__origin__"): 55 # For parameterized generics, check against the origin type 56 if isinstance(self.value, type_arg.__origin__): 57 return True 58 elif isinstance(self.value, (self.type, type_arg)): 59 return True 60 msg = f"Value {self.value} is not any of {type_args}." 61 raise InvalidConverterError(msg) 62 msg = "This should not have raised. Report to the library maintainers with code: `DTBDT`" 63 raise TypeError(msg) 64 65 @staticmethod 66 def cast_optional(default: T | None | BaseDataType[T]) -> BaseDataType[T | None]: 67 """Convert the default value to an Optional data type.""" 68 if default is None: 69 return cast("BaseDataType[T | None]", NoneType()) 70 return Optional(BaseDataType.cast(default)) 71 72 @staticmethod 73 def cast(default: T | BaseDataType[T]) -> BaseDataType[T]: # noqa: C901, PLR0911 74 """Convert the default value to a BaseDataType.""" 75 # We use Cast to shut up type checkers, as we know primitive types will be correct. 76 # If a custom type is passed, it should be a BaseDataType subclass, which already has the correct types. 77 # Check enum types BEFORE basic types since some enums inherit from str/int 78 match default: 79 case dStrEnum(): return cast("BaseDataType[T]", StrEnum(default)) 80 case dIntFlag(): return cast("BaseDataType[T]", IntFlag(default)) 81 case dIntEnum(): return cast("BaseDataType[T]", IntEnum(default)) 82 case dEnum(): return cast("BaseDataType[T]", Enum(default)) 83 case bool(): return cast("BaseDataType[T]", Boolean(default)) 84 case None: return cast("BaseDataType[T]", NoneType()) 85 case int(): return cast("BaseDataType[T]", Integer(default)) 86 case float(): return cast("BaseDataType[T]", Float(default)) 87 case str(): return cast("BaseDataType[T]", String(default)) 88 case BaseDataType(): return default 89 case _: 90 msg = ( 91 f"Unsupported default value type: {type(default).__name__}. " 92 "Use a BaseDataType subclass for custom types." 93 ) 94 raise InvalidDefaultError(msg)
Base class used for Config descriptors to define a data type.
26 def __init__(self, default: T) -> None: 27 """Initialize the base data type.""" 28 self.default = default 29 self.value = default 30 self.type = type(default)
Initialize the base data type.
36 @abstractmethod 37 def convert(self, value: str) -> T: 38 """Convert a string value to the desired type."""
Convert a string value to the desired type.
40 def validate(self) -> bool: 41 """Validate that the value matches the expected type.""" 42 orig_bases: tuple[type, ...] | None = getattr(self.__class__, "__orig_bases__", None) 43 44 if not orig_bases: 45 msg = "No type information available for validation." 46 raise InvalidConverterError(msg) 47 48 # Extract type arguments from the generic base 49 for base in orig_bases: 50 if hasattr(base, "__args__"): 51 type_args = base.__args__ 52 if type_args: 53 for type_arg in type_args: 54 if hasattr(type_arg, "__origin__"): 55 # For parameterized generics, check against the origin type 56 if isinstance(self.value, type_arg.__origin__): 57 return True 58 elif isinstance(self.value, (self.type, type_arg)): 59 return True 60 msg = f"Value {self.value} is not any of {type_args}." 61 raise InvalidConverterError(msg) 62 msg = "This should not have raised. Report to the library maintainers with code: `DTBDT`" 63 raise TypeError(msg)
Validate that the value matches the expected type.
65 @staticmethod 66 def cast_optional(default: T | None | BaseDataType[T]) -> BaseDataType[T | None]: 67 """Convert the default value to an Optional data type.""" 68 if default is None: 69 return cast("BaseDataType[T | None]", NoneType()) 70 return Optional(BaseDataType.cast(default))
Convert the default value to an Optional data type.
72 @staticmethod 73 def cast(default: T | BaseDataType[T]) -> BaseDataType[T]: # noqa: C901, PLR0911 74 """Convert the default value to a BaseDataType.""" 75 # We use Cast to shut up type checkers, as we know primitive types will be correct. 76 # If a custom type is passed, it should be a BaseDataType subclass, which already has the correct types. 77 # Check enum types BEFORE basic types since some enums inherit from str/int 78 match default: 79 case dStrEnum(): return cast("BaseDataType[T]", StrEnum(default)) 80 case dIntFlag(): return cast("BaseDataType[T]", IntFlag(default)) 81 case dIntEnum(): return cast("BaseDataType[T]", IntEnum(default)) 82 case dEnum(): return cast("BaseDataType[T]", Enum(default)) 83 case bool(): return cast("BaseDataType[T]", Boolean(default)) 84 case None: return cast("BaseDataType[T]", NoneType()) 85 case int(): return cast("BaseDataType[T]", Integer(default)) 86 case float(): return cast("BaseDataType[T]", Float(default)) 87 case str(): return cast("BaseDataType[T]", String(default)) 88 case BaseDataType(): return default 89 case _: 90 msg = ( 91 f"Unsupported default value type: {type(default).__name__}. " 92 "Use a BaseDataType subclass for custom types." 93 ) 94 raise InvalidDefaultError(msg)
Convert the default value to a BaseDataType.
327class Binary(BaseDataType[bytes | int]): 328 """A config value that represents binary.""" 329 330 def __init__(self, default: bytes | int = 0) -> None: # noqa: D107 331 if isinstance(default, bytes): 332 default = int.from_bytes(default) 333 super().__init__(default) 334 335 def __str__(self) -> str: # noqa: D105 336 if isinstance(self.value, bytes): 337 self.value = int.from_bytes(self.value) 338 return f"0b{self.value:b}" 339 340 def convert(self, value: str) -> int: 341 """Convert a string value to an integer from binary.""" 342 return int(value.removeprefix("0b"), 2)
A config value that represents binary.
243class Boolean(BaseDataType[bool]): 244 """A config value that is a boolean.""" 245 246 def __init__(self, default: bool = False) -> None: # noqa: D107, FBT001, FBT002 247 super().__init__(default) 248 249 def convert(self, value: str) -> bool: 250 """Convert a string value to a boolean.""" 251 if value.lower() in {"true", "1", "yes"}: 252 return True 253 if value.lower() in {"false", "0", "no"}: 254 return False 255 msg = f"Cannot convert {value} to boolean." 256 raise ValueError(msg)
A config value that is a boolean.
246 def __init__(self, default: bool = False) -> None: # noqa: D107, FBT001, FBT002 247 super().__init__(default)
Initialize the base data type.
249 def convert(self, value: str) -> bool: 250 """Convert a string value to a boolean.""" 251 if value.lower() in {"true", "1", "yes"}: 252 return True 253 if value.lower() in {"false", "0", "no"}: 254 return False 255 msg = f"Cannot convert {value} to boolean." 256 raise ValueError(msg)
Convert a string value to a boolean.
48class Config(Generic[VT]): 49 """A descriptor for config values, preserving type information. 50 51 the ValueType (VT) is the type you want the config value to be. 52 """ 53 54 validate_types: ClassVar[bool] = True 55 """Validate that the converter returns the same type as the default value. (not strict)""" 56 write_on_edit: ClassVar[bool] = True 57 """Write to the config file when updating a value.""" 58 optional: bool = False 59 """If True, allows None as an extra type when validating types. (both instance and class variables.)""" 60 61 _parser: ConfkitParser = UNSET 62 _file: Path = UNSET 63 _has_read_config: bool = False 64 _data_type: BaseDataType[VT] 65 66 if TYPE_CHECKING: 67 # Overloads for type checkers to understand the different settings of the Config descriptors. 68 @overload # Custom data type, like Enum's or custom class. 69 def __init__(self, default: BaseDataType[VT]) -> None: ... 70 @overload 71 def __init__(self, default: VT) -> None: ... 72 # Specify the states of optional explicitly for type checkers. 73 @overload 74 def __init__(self: Config[OVT], default: OVT, *, optional: Literal[False]) -> None: ... 75 @overload 76 def __init__(self: Config[OVT], default: BaseDataType[OVT], *, optional: Literal[False]) -> None: ... 77 @overload 78 def __init__(self: Config[OVT | None], default: OVT, *, optional: Literal[True]) -> None: ... 79 @overload 80 def __init__(self: Config[OVT | None], default: BaseDataType[OVT], *, optional: Literal[True]) -> None: ... 81 82 def __init__( 83 self, 84 default: VT | None | BaseDataType[VT] = UNSET, 85 *, 86 optional: bool = False, 87 ) -> None: 88 """Initialize the config descriptor with a default value. 89 90 Validate that parser and filepath are present. 91 """ 92 cls = self.__class__ 93 self.optional = optional or cls.optional # Be truthy when either one is true. 94 95 if not self.optional and default is UNSET: 96 msg = "Default value cannot be None when optional is False." 97 raise InvalidDefaultError(msg) 98 99 if not self._parser: 100 self._detect_parser() 101 102 self._initialize_data_type(default) 103 self._validate_init() 104 self._read_parser() 105 106 def __init_subclass__(cls) -> None: 107 """Allow for multiple config files/parsers without conflicts.""" 108 super().__init_subclass__() 109 110 parent = cls._find_parent() 111 112 cls.validate_types = parent.validate_types 113 cls.write_on_edit = parent.write_on_edit 114 cls._parser = parent._parser # noqa: SLF001 115 cls._file = parent._file # noqa: SLF001 116 cls._has_read_config = parent._has_read_config # noqa: SLF001 117 118 @classmethod 119 def _find_parent(cls) -> type[Config[Any]]: 120 for base in cls.__bases__: 121 if issubclass(base, Config): 122 parent = base 123 break 124 else: 125 parent = Config 126 return parent 127 128 def _initialize_data_type(self, default: VT | None | BaseDataType[VT]) -> None: 129 """Initialize the data type based on the default value.""" 130 if not self.optional and default is not None: 131 self._data_type = BaseDataType[VT].cast(default) 132 else: 133 self._data_type = BaseDataType[VT].cast_optional(default) 134 135 def _read_parser(self) -> None: 136 """Ensure the parser has read the file at initialization. Avoids rewriting the file when settings are already set.""" 137 cls = self.__class__ 138 if not cls._has_read_config: 139 self._parser.read(self._file) 140 cls._has_read_config = True 141 142 def _validate_init(self) -> None: 143 """Validate the config descriptor, ensuring it's properly set up.""" 144 self.validate_file() 145 self.validate_parser() 146 147 def convert(self, value: str) -> VT: 148 """Convert the value to the desired type using the given converter method.""" 149 return self._data_type.convert(value) 150 151 @staticmethod 152 def _warn_base_class_usage() -> None: 153 """Warn users that setting parser/file on the base class can lead to unexpected behavior. 154 Tell the user to subclass <Config> first. 155 """ # noqa: D205 156 warnings.warn("<Config> is the base class. Subclass <Config> to avoid unexpected behavior.", stacklevel=2) 157 158 @classmethod 159 @deprecated("Avoid using set_parser. Confkit will automatically assign a parser based on the file extension. In 2.0 this will be a private method.") # noqa: E501 160 def set_parser(cls, parser: ConfkitParser) -> None: 161 """Set the parser for ALL descriptor instances (of this type/class).""" 162 if cls is Config: 163 cls._warn_base_class_usage() 164 cls._parser = parser 165 166 @classmethod 167 def _detect_parser(cls) -> None: 168 """Set the parser for descriptors based on the file extension of cls._file. 169 170 Uses msgspec-based parsers for yaml, json, toml. Defaults to dict structure. 171 Only sets the parser if there is no parser set. 172 """ 173 if cls._file is UNSET: 174 msg = "Config file is not set. Use `set_file()`." 175 raise ValueError(msg) 176 match cls._file.suffix.lower(): 177 case ".ini": 178 cls._parser = IniParser() 179 case ".yaml" | ".yml" | ".json" | ".toml": 180 from confkit.ext.parsers import MsgspecParser # noqa: PLC0415 Only import if actually used. 181 cls._parser = MsgspecParser() 182 case ".env": 183 cls._parser = EnvParser() 184 case _: 185 msg = f"Unsupported config file extension: {cls._file.suffix.lower()}" 186 raise ValueError(msg) 187 188 @classmethod 189 def set_file(cls, file: Path) -> None: 190 """Set the file for ALL descriptors.""" 191 if cls is Config: 192 cls._warn_base_class_usage() 193 cls._file = file 194 cls._watcher = FileWatcher(file) 195 196 def validate_strict_type(self) -> None: 197 """Validate the type of the converter matches the desired type.""" 198 if self._data_type.convert is UNSET: 199 msg = "Converter is not set." 200 raise InvalidConverterError(msg) 201 202 cls = self.__class__ 203 self.__config_value = cls._parser.get(self._section, self._setting) 204 self.__converted_value = self.convert(self.__config_value) 205 206 if not cls.validate_types: 207 return 208 209 self.__converted_type = type(self.__converted_value) 210 default_value_type = type(self._data_type.default) 211 212 is_optional = self.optional or isinstance(self._data_type, Optional) 213 if (is_optional) and self.__converted_type in (default_value_type, NoneType): 214 # Allow None or the same type as the default value to be returned by the converter when _optional is True. 215 return 216 if self.__converted_type is not default_value_type: 217 msg = f"Converter does not return the same type as the default value <{default_value_type}> got <{self.__converted_type}>." # noqa: E501 218 raise InvalidConverterError(msg) 219 220 # Set the data_type value. ensuring validation works as expected. 221 self._data_type.value = self.__converted_value 222 if not self._data_type.validate(): 223 msg = f"Invalid value for {self._section}.{self._setting}: {self.__converted_value}" 224 raise InvalidConverterError(msg) 225 226 @classmethod 227 def validate_file(cls) -> None: 228 """Validate the config file.""" 229 if cls._file is UNSET: 230 msg = f"Config file is not set. use {cls.__name__}.set_file() to set it." 231 raise ValueError(msg) 232 233 @classmethod 234 def validate_parser(cls) -> None: 235 """Validate the config parser.""" 236 if cls._parser is UNSET: 237 msg = f"Config parser is not set. use {cls.__name__}.set_parser() to set it." 238 raise ValueError(msg) 239 240 def __set_name__(self, owner: type, name: str) -> None: 241 """Set the name of the attribute to the name of the descriptor.""" 242 self.name = name 243 self._section = self._build_section_name(owner) 244 self._setting = name 245 self._ensure_option() 246 cls = self.__class__ 247 self._original_value = cls._parser.get(self._section, self._setting) or self._data_type.default 248 self.private = f"_{self._section}_{self._setting}_{self.name}" 249 250 @staticmethod 251 def _build_section_name(owner: type) -> str: 252 """Build a section name from the class hierarchy using dot notation. 253 254 Strips out function-local scope markers like <locals>. 255 """ 256 if qualname := getattr(owner, "__qualname__", None): 257 split_at = qualname.find("<locals>.") 258 if split_at != -1: 259 qualname = qualname[split_at + len("<locals>.") :] 260 return ".".join( 261 part 262 for part in qualname.split(".") 263 ) 264 return owner.__name__ 265 266 def _ensure_section(self) -> None: 267 """Ensure the section exists in the config file. Creates one if it doesn't exist.""" 268 if not self._parser.has_section(self._section): 269 self._parser.add_section(self._section) 270 271 def _ensure_option(self) -> None: 272 """Ensure the option exists in the config file. Creates one if it doesn't exist.""" 273 self._ensure_section() 274 if not self._parser.has_option(self._section, self._setting): 275 cls = self.__class__ 276 cls._set(self._section, self._setting, self._data_type) 277 278 def __get__(self, obj: object, obj_type: object) -> VT: 279 """Get the value of the attribute.""" 280 # obj_type is the class in which the variable is defined 281 # so it can be different than type of VT 282 # but we don't need obj or it's type to get the value from config in our case. 283 if self._watcher.has_changed(): 284 self.on_file_change( 285 "get", 286 self._data_type.value, 287 self.convert(self._parser.get(self._section, self._setting)), 288 ) 289 290 self.validate_strict_type() 291 return self.__converted_value # This is already used when checking type validation, so it's safe to return it. 292 293 def __set__(self, obj: object, value: VT) -> None: 294 """Set the value of the attribute.""" 295 if self._watcher.has_changed(): 296 self.on_file_change("set", self._data_type.value, value) 297 298 self._data_type.value = value 299 cls = self.__class__ 300 cls._set(self._section, self._setting, self._data_type) 301 setattr(obj, self.private, value) 302 303 @classmethod 304 def _set(cls, section: str, setting: str, value: VT | BaseDataType[VT] | BaseDataType[VT | None]) -> None: 305 """Set a config value, and write it to the file.""" 306 if not cls._parser.has_section(section): 307 cls._parser.add_section(section) 308 309 cls._parser.set(section, setting, value) 310 311 if cls.write_on_edit: 312 cls.write() 313 314 315 @classmethod 316 def write(cls) -> None: 317 """Write the config parser to the file.""" 318 cls.validate_file() 319 with cls._file.open("w") as f: 320 cls._parser.write(f) 321 322 @classmethod 323 def set(cls, section: str, setting: str, value: VT) -> Callable[[Callable[P, F]], Callable[P, F]]: 324 """Set a config value using this descriptor.""" 325 326 def wrapper(func: Callable[P, F]) -> Callable[P, F]: 327 @wraps(func) 328 def inner(*args: P.args, **kwargs: P.kwargs) -> F: 329 cls._set(section, setting, value) 330 return func(*args, **kwargs) 331 332 return inner 333 return wrapper 334 335 336 @classmethod 337 def with_setting(cls, setting: Config[OVT]) -> Callable[[Callable[P, F]], Callable[P, F]]: 338 """Insert a config value into **kwargs to the wrapped method/function using this decorator.""" 339 def wrapper(func: Callable[P, F]) -> Callable[P, F]: 340 @wraps(func) 341 def inner(*args: P.args, **kwargs: P.kwargs) -> F: 342 kwargs[setting.name] = setting.convert(cls._parser.get(setting._section, setting._setting)) 343 return func(*args, **kwargs) 344 345 return inner 346 return wrapper 347 348 349 @classmethod 350 def with_kwarg( 351 cls, section: str, setting: str, name: str | None = None, default: VT = UNSET, 352 ) -> Callable[[Callable[P, F]], Callable[P, F]]: 353 """Insert a config value into **kwargs to the wrapped method/function using this descriptor. 354 355 Use kwarg.get(`name`) to get the value. 356 `name` is the name the kwarg gets if passed, if None, it will be the same as `setting`. 357 Section parameter is just for finding the config value. 358 """ 359 if name is None: 360 name = setting 361 if default is UNSET and not cls._parser.has_option(section, setting): 362 msg = f"Config value {section=} {setting=} is not set. and no default value is given." 363 raise ValueError(msg) 364 365 def wrapper(func: Callable[P, F]) -> Callable[P, F]: 366 @wraps(func) 367 def inner(*args: P.args, **kwargs: P.kwargs) -> F: 368 if default is not UNSET: 369 cls._set_default(section, setting, default) 370 kwargs[name] = cls._parser.get(section, setting) 371 return func(*args, **kwargs) 372 373 return inner 374 return wrapper 375 376 @classmethod 377 def _set_default(cls, section: str, setting: str, value: VT) -> None: 378 if cls._parser.get(section, setting, fallback=UNSET) is UNSET: 379 cls._set(section, setting, value) 380 381 @classmethod 382 def default(cls, section: str, setting: str, value: VT) -> Callable[[Callable[P, F]], Callable[P, F]]: 383 """Set a default config value if none are set yet using this descriptor.""" 384 def wrapper(func: Callable[P, F]) -> Callable[P, F]: 385 @wraps(func) 386 def inner(*args: P.args, **kwargs: P.kwargs) -> F: 387 cls._set_default(section, setting, value) 388 return func(*args, **kwargs) 389 390 return inner 391 return wrapper 392 393 @abstractmethod 394 def on_file_change(self, origin: Literal["get", "set"], old: VT | UNSET, new: VT) -> None: 395 """Triggered when the config file changes. 396 397 This needs to be implemented before it's usable. 398 This will be called **before** setting the value from the config file. 399 This will be called **after** getting (but before validating it's type) the value from config file. 400 The `origin` parameter indicates whether the change was triggered by a `get` or `set` operation. 401 """
A descriptor for config values, preserving type information.
the ValueType (VT) is the type you want the config value to be.
82 def __init__( 83 self, 84 default: VT | None | BaseDataType[VT] = UNSET, 85 *, 86 optional: bool = False, 87 ) -> None: 88 """Initialize the config descriptor with a default value. 89 90 Validate that parser and filepath are present. 91 """ 92 cls = self.__class__ 93 self.optional = optional or cls.optional # Be truthy when either one is true. 94 95 if not self.optional and default is UNSET: 96 msg = "Default value cannot be None when optional is False." 97 raise InvalidDefaultError(msg) 98 99 if not self._parser: 100 self._detect_parser() 101 102 self._initialize_data_type(default) 103 self._validate_init() 104 self._read_parser()
Initialize the config descriptor with a default value.
Validate that parser and filepath are present.
Validate that the converter returns the same type as the default value. (not strict)
If True, allows None as an extra type when validating types. (both instance and class variables.)
147 def convert(self, value: str) -> VT: 148 """Convert the value to the desired type using the given converter method.""" 149 return self._data_type.convert(value)
Convert the value to the desired type using the given converter method.
158 @classmethod 159 @deprecated("Avoid using set_parser. Confkit will automatically assign a parser based on the file extension. In 2.0 this will be a private method.") # noqa: E501 160 def set_parser(cls, parser: ConfkitParser) -> None: 161 """Set the parser for ALL descriptor instances (of this type/class).""" 162 if cls is Config: 163 cls._warn_base_class_usage() 164 cls._parser = parser
Set the parser for ALL descriptor instances (of this type/class).
188 @classmethod 189 def set_file(cls, file: Path) -> None: 190 """Set the file for ALL descriptors.""" 191 if cls is Config: 192 cls._warn_base_class_usage() 193 cls._file = file 194 cls._watcher = FileWatcher(file)
Set the file for ALL descriptors.
196 def validate_strict_type(self) -> None: 197 """Validate the type of the converter matches the desired type.""" 198 if self._data_type.convert is UNSET: 199 msg = "Converter is not set." 200 raise InvalidConverterError(msg) 201 202 cls = self.__class__ 203 self.__config_value = cls._parser.get(self._section, self._setting) 204 self.__converted_value = self.convert(self.__config_value) 205 206 if not cls.validate_types: 207 return 208 209 self.__converted_type = type(self.__converted_value) 210 default_value_type = type(self._data_type.default) 211 212 is_optional = self.optional or isinstance(self._data_type, Optional) 213 if (is_optional) and self.__converted_type in (default_value_type, NoneType): 214 # Allow None or the same type as the default value to be returned by the converter when _optional is True. 215 return 216 if self.__converted_type is not default_value_type: 217 msg = f"Converter does not return the same type as the default value <{default_value_type}> got <{self.__converted_type}>." # noqa: E501 218 raise InvalidConverterError(msg) 219 220 # Set the data_type value. ensuring validation works as expected. 221 self._data_type.value = self.__converted_value 222 if not self._data_type.validate(): 223 msg = f"Invalid value for {self._section}.{self._setting}: {self.__converted_value}" 224 raise InvalidConverterError(msg)
Validate the type of the converter matches the desired type.
226 @classmethod 227 def validate_file(cls) -> None: 228 """Validate the config file.""" 229 if cls._file is UNSET: 230 msg = f"Config file is not set. use {cls.__name__}.set_file() to set it." 231 raise ValueError(msg)
Validate the config file.
233 @classmethod 234 def validate_parser(cls) -> None: 235 """Validate the config parser.""" 236 if cls._parser is UNSET: 237 msg = f"Config parser is not set. use {cls.__name__}.set_parser() to set it." 238 raise ValueError(msg)
Validate the config parser.
315 @classmethod 316 def write(cls) -> None: 317 """Write the config parser to the file.""" 318 cls.validate_file() 319 with cls._file.open("w") as f: 320 cls._parser.write(f)
Write the config parser to the file.
322 @classmethod 323 def set(cls, section: str, setting: str, value: VT) -> Callable[[Callable[P, F]], Callable[P, F]]: 324 """Set a config value using this descriptor.""" 325 326 def wrapper(func: Callable[P, F]) -> Callable[P, F]: 327 @wraps(func) 328 def inner(*args: P.args, **kwargs: P.kwargs) -> F: 329 cls._set(section, setting, value) 330 return func(*args, **kwargs) 331 332 return inner 333 return wrapper
Set a config value using this descriptor.
336 @classmethod 337 def with_setting(cls, setting: Config[OVT]) -> Callable[[Callable[P, F]], Callable[P, F]]: 338 """Insert a config value into **kwargs to the wrapped method/function using this decorator.""" 339 def wrapper(func: Callable[P, F]) -> Callable[P, F]: 340 @wraps(func) 341 def inner(*args: P.args, **kwargs: P.kwargs) -> F: 342 kwargs[setting.name] = setting.convert(cls._parser.get(setting._section, setting._setting)) 343 return func(*args, **kwargs) 344 345 return inner 346 return wrapper
Insert a config value into **kwargs to the wrapped method/function using this decorator.
349 @classmethod 350 def with_kwarg( 351 cls, section: str, setting: str, name: str | None = None, default: VT = UNSET, 352 ) -> Callable[[Callable[P, F]], Callable[P, F]]: 353 """Insert a config value into **kwargs to the wrapped method/function using this descriptor. 354 355 Use kwarg.get(`name`) to get the value. 356 `name` is the name the kwarg gets if passed, if None, it will be the same as `setting`. 357 Section parameter is just for finding the config value. 358 """ 359 if name is None: 360 name = setting 361 if default is UNSET and not cls._parser.has_option(section, setting): 362 msg = f"Config value {section=} {setting=} is not set. and no default value is given." 363 raise ValueError(msg) 364 365 def wrapper(func: Callable[P, F]) -> Callable[P, F]: 366 @wraps(func) 367 def inner(*args: P.args, **kwargs: P.kwargs) -> F: 368 if default is not UNSET: 369 cls._set_default(section, setting, default) 370 kwargs[name] = cls._parser.get(section, setting) 371 return func(*args, **kwargs) 372 373 return inner 374 return wrapper
Insert a config value into **kwargs to the wrapped method/function using this descriptor.
Use kwarg.get(name) to get the value.
name is the name the kwarg gets if passed, if None, it will be the same as setting.
Section parameter is just for finding the config value.
381 @classmethod 382 def default(cls, section: str, setting: str, value: VT) -> Callable[[Callable[P, F]], Callable[P, F]]: 383 """Set a default config value if none are set yet using this descriptor.""" 384 def wrapper(func: Callable[P, F]) -> Callable[P, F]: 385 @wraps(func) 386 def inner(*args: P.args, **kwargs: P.kwargs) -> F: 387 cls._set_default(section, setting, value) 388 return func(*args, **kwargs) 389 390 return inner 391 return wrapper
Set a default config value if none are set yet using this descriptor.
393 @abstractmethod 394 def on_file_change(self, origin: Literal["get", "set"], old: VT | UNSET, new: VT) -> None: 395 """Triggered when the config file changes. 396 397 This needs to be implemented before it's usable. 398 This will be called **before** setting the value from the config file. 399 This will be called **after** getting (but before validating it's type) the value from config file. 400 The `origin` parameter indicates whether the change was triggered by a `get` or `set` operation. 401 """
Triggered when the config file changes.
This needs to be implemented before it's usable.
This will be called before setting the value from the config file.
This will be called after getting (but before validating it's type) the value from config file.
The origin parameter indicates whether the change was triggered by a get or set operation.
37class ConfigContainerMeta(type): 38 """Metaclass for Config to "force" __set__ to be called on class variables.""" 39 40 def __setattr__(cls, key: str, value: object) -> None: 41 """Set the value of the attribute on the class.""" 42 attr = cls.__dict__.get(key) 43 if isinstance(attr, Config): 44 attr.__set__(cls, value) 45 else: 46 super().__setattr__(key, value)
Metaclass for Config to "force" __set__ to be called on class variables.
14class ConfigPathConflictError(ValueError): 15 """Raised when a configuration path conflicts with an existing scalar value. 16 17 This occurs when attempting to treat a scalar value as a section (dict). 18 For example, if "Parent.Value" is a scalar, attempting to set "Parent.Value.Child" 19 would cause this error. 20 """
Raised when a configuration path conflicts with an existing scalar value.
This occurs when attempting to treat a scalar value as a section (dict). For example, if "Parent.Value" is a scalar, attempting to set "Parent.Value.Child" would cause this error.
655class Date(BaseDataType[date]): 656 """A config value that is a date.""" 657 658 @overload 659 def __init__(self, default: date = UNSET) -> None: ... 660 @overload 661 def __init__(self, **kwargs: Unpack[_DateKwargs]) -> None: ... 662 663 def __init__(self, default: date = UNSET, **kwargs: Unpack[_DateKwargs]) -> None: 664 """Initialize the date data type. Defaults to current date if not provided.""" 665 if default is UNSET: 666 default = date(**kwargs) 667 super().__init__(default) 668 669 def convert(self, value: str) -> date: 670 """Convert a string value to a date.""" 671 return date.fromisoformat(value) 672 673 def __str__(self) -> str: # noqa: D105 674 return self.value.isoformat()
A config value that is a date.
663 def __init__(self, default: date = UNSET, **kwargs: Unpack[_DateKwargs]) -> None: 664 """Initialize the date data type. Defaults to current date if not provided.""" 665 if default is UNSET: 666 default = date(**kwargs) 667 super().__init__(default)
Initialize the date data type. Defaults to current date if not provided.
625class DateTime(BaseDataType[datetime]): 626 """A config value that is a datetime.""" 627 628 @overload 629 def __init__(self, default: datetime = UNSET) -> None: ... 630 @overload 631 def __init__(self, **kwargs: Unpack[_DateTimeKwargs]) -> None: ... 632 633 def __init__(self, default: datetime = UNSET, **kwargs: Unpack[_DateTimeKwargs]) -> None: 634 """Initialize the datetime data type. Defaults to current datetime (datetime.now) if not provided.""" 635 if default is UNSET: 636 try: 637 default = datetime(**kwargs) # noqa: DTZ001 Tzinfo is (optionally) passed using kwargs 638 except TypeError: 639 default = datetime.now(tz=UTC) 640 super().__init__(default) 641 642 def convert(self, value: str) -> datetime: 643 """Convert a string value to a datetime.""" 644 return datetime.fromisoformat(value) 645 646 def __str__(self) -> str: 647 """Return the string representation of the stored value.""" 648 return self.value.isoformat()
A config value that is a datetime.
633 def __init__(self, default: datetime = UNSET, **kwargs: Unpack[_DateTimeKwargs]) -> None: 634 """Initialize the datetime data type. Defaults to current datetime (datetime.now) if not provided.""" 635 if default is UNSET: 636 try: 637 default = datetime(**kwargs) # noqa: DTZ001 Tzinfo is (optionally) passed using kwargs 638 except TypeError: 639 default = datetime.now(tz=UTC) 640 super().__init__(default)
Initialize the datetime data type. Defaults to current datetime (datetime.now) if not provided.
531class Dict(BaseDataType[dict[KT, VT]], Generic[KT, VT]): 532 """A config value that is a dictionary of string keys and values of type T.""" 533 534 @overload 535 def __init__(self, default: dict[KT, VT]) -> None: ... 536 @overload 537 def __init__(self, *, key_type: BaseDataType[KT], value_type: BaseDataType[VT]) -> None: ... 538 @overload 539 def __init__( 540 self, 541 default: dict[KT, VT], 542 *, 543 key_type: BaseDataType[KT] = ..., 544 value_type: BaseDataType[VT] = ..., 545 ) -> None: ... 546 547 def __init__( 548 self, 549 default: dict[KT, VT] = UNSET, 550 *, 551 key_type: BaseDataType[KT] = UNSET, 552 value_type: BaseDataType[VT] = UNSET, 553 ) -> None: 554 """Initialize the dict data type.""" 555 if default is UNSET and (key_type is UNSET or value_type is UNSET): 556 msg = "Dict requires either a default with at least one key/value pair, or both key_type and value_type to be specified." # noqa: E501 557 raise InvalidDefaultError(msg) 558 if default is UNSET: 559 default = {} 560 super().__init__(default) 561 562 self._infer_key_type(default, key_type) 563 self._infer_value_type(default, value_type) 564 565 def _infer_key_type(self, default: dict[KT, VT], key_type: BaseDataType[KT]) -> None: 566 """Infer the key type from the default dictionary if not provided.""" 567 if len(default.keys()) <= 0 and key_type is UNSET: 568 msg = "Dict default must have at least one key element to infer type. or specify `key_type=<BaseDataType>`" 569 raise InvalidDefaultError(msg) 570 if key_type is UNSET: 571 for key in default: 572 self._key_data_type = BaseDataType[KT].cast(key) 573 break 574 else: 575 self._key_data_type = key_type 576 577 def _infer_value_type(self, default: dict[KT, VT], value_type: BaseDataType[VT]) -> None: 578 """Infer the value type from the default dictionary if not provided.""" 579 if len(default.values()) <= 0 and value_type is UNSET: 580 msg = "Dict default must have at least one value element to infer type. or specify `value_type=<BaseDataType>`" 581 raise InvalidDefaultError(msg) 582 if value_type is UNSET: 583 for value in default.values(): 584 self._value_data_type = BaseDataType[VT].cast(value) 585 break 586 else: 587 self._value_data_type = value_type 588 589 def convert(self, value: str) -> dict[KT, VT]: 590 """Convert a string to a dictionary.""" 591 if not value: 592 return {} 593 594 parts = value.split(",") 595 result: dict[KT, VT] = {} 596 for part in parts: 597 if "=" not in part: 598 msg = f"Invalid dictionary entry: {part}. Expected format key=value." 599 raise ValueError(msg) 600 key_str, val_str = part.split("=", 1) 601 key = self._key_data_type.convert(key_str.strip()) 602 val = self._value_data_type.convert(val_str.strip()) 603 result[key] = val 604 return result 605 606 def __str__(self) -> str: 607 """Return a string representation of the dictionary.""" 608 items = [ 609 f"{self._key_data_type.convert(str(k))}={self._value_data_type.convert(str(v))}" 610 for k, v in self.value.items() 611 ] 612 return ",".join(items)
A config value that is a dictionary of string keys and values of type T.
547 def __init__( 548 self, 549 default: dict[KT, VT] = UNSET, 550 *, 551 key_type: BaseDataType[KT] = UNSET, 552 value_type: BaseDataType[VT] = UNSET, 553 ) -> None: 554 """Initialize the dict data type.""" 555 if default is UNSET and (key_type is UNSET or value_type is UNSET): 556 msg = "Dict requires either a default with at least one key/value pair, or both key_type and value_type to be specified." # noqa: E501 557 raise InvalidDefaultError(msg) 558 if default is UNSET: 559 default = {} 560 super().__init__(default) 561 562 self._infer_key_type(default, key_type) 563 self._infer_value_type(default, value_type)
Initialize the dict data type.
589 def convert(self, value: str) -> dict[KT, VT]: 590 """Convert a string to a dictionary.""" 591 if not value: 592 return {} 593 594 parts = value.split(",") 595 result: dict[KT, VT] = {} 596 for part in parts: 597 if "=" not in part: 598 msg = f"Invalid dictionary entry: {part}. Expected format key=value." 599 raise ValueError(msg) 600 key_str, val_str = part.split("=", 1) 601 key = self._key_data_type.convert(key_str.strip()) 602 val = self._value_data_type.convert(val_str.strip()) 603 result[key] = val 604 return result
Convert a string to a dictionary.
128class Enum(_EnumBase[EnumType]): 129 """A config value that is an enum.""" 130 131 def convert(self, value: str) -> EnumType: 132 """Convert a string value to an enum.""" 133 value = self._strip_comment(value) 134 parsed_enum_name = value.split(".")[-1] 135 return self.value.__class__[parsed_enum_name] 136 137 def _format_allowed_values(self) -> str: 138 """Format allowed values as comma-separated member names.""" 139 enum_class = self.value.__class__ 140 return ", ".join(member.name for member in enum_class) 141 142 def _get_value_str(self) -> str: 143 """Get the member name.""" 144 return self.value.name
A config value that is an enum.
131 def convert(self, value: str) -> EnumType: 132 """Convert a string value to an enum.""" 133 value = self._strip_comment(value) 134 parsed_enum_name = value.split(".")[-1] 135 return self.value.__class__[parsed_enum_name]
Convert a string value to an enum.
232class Float(BaseDataType[float]): 233 """A config value that is a float.""" 234 235 def __init__(self, default: float = 0.0) -> None: # noqa: D107 236 super().__init__(default) 237 238 def convert(self, value: str) -> float: 239 """Convert a string value to a float.""" 240 return float(value)
A config value that is a float.
301class Hex(Integer): 302 """A config value that represents hexadecimal.""" 303 304 def __init__(self, default: int = 0, base: int = HEXADECIMAL) -> None: # noqa: D107 305 super().__init__(default, base) 306 307 def __str__(self) -> str: # noqa: D105 308 return f"0x{self.value:x}" 309 310 def convert(self, value: str) -> int: 311 """Convert a string value to an integer. from hexadecimal.""" 312 return int(value.removeprefix("0x"), 16)
A config value that represents hexadecimal.
165class IntEnum(_EnumBase[IntEnumType]): 166 """A config value that is an enum.""" 167 168 def convert(self, value: str) -> IntEnumType: 169 """Convert a string value to an enum.""" 170 value = self._strip_comment(value) 171 return self.value.__class__(int(value)) 172 173 def _format_allowed_values(self) -> str: 174 """Format allowed values as comma-separated name(value) pairs.""" 175 enum_class = self.value.__class__ 176 return ", ".join(f"{member.name}({member.value})" for member in enum_class) 177 178 def _get_value_str(self) -> str: 179 """Get the member value as string.""" 180 return str(self.value.value)
A config value that is an enum.
183class IntFlag(_EnumBase[IntFlagType]): 184 """A config value that is an enum.""" 185 186 def convert(self, value: str) -> IntFlagType: 187 """Convert a string value to an enum.""" 188 value = self._strip_comment(value) 189 return self.value.__class__(int(value)) 190 191 def _format_allowed_values(self) -> str: 192 """Format allowed values as comma-separated name(value) pairs.""" 193 enum_class = self.value.__class__ 194 return ", ".join(f"{member.name}({member.value})" for member in enum_class) 195 196 def _get_value_str(self) -> str: 197 """Get the member value as string.""" 198 return str(self.value.value)
A config value that is an enum.
263class Integer(BaseDataType[int]): 264 """A config value that is an integer.""" 265 266 # Define constants for common bases 267 268 def __init__(self, default: int = 0, base: int = DECIMAL) -> None: # noqa: D107 269 super().__init__(default) 270 self.base = base 271 272 @staticmethod 273 def int_to_base(number: int, base: int) -> int: 274 """Convert an integer to a string representation in a given base.""" 275 if number == 0: 276 return 0 277 digits = [] 278 while number: 279 digits.append(str(number % base)) 280 number //= base 281 return int("".join(reversed(digits))) 282 283 def __str__(self) -> str: # noqa: D105 284 if self.base == DECIMAL: 285 return str(self.value) 286 # Convert the base 10 int to base 5 287 self.value = self.int_to_base(int(self.value), self.base) 288 return f"{self.base}c{self.value}" 289 290 def convert(self, value: str) -> int: 291 """Convert a string value to an integer.""" 292 if "c" in value: 293 base_str, val_str = value.split("c") 294 base = int(base_str) 295 if base != self.base: 296 msg = "Base in string does not match base in Integer while converting." 297 raise ValueError(msg) 298 return int(val_str, self.base) 299 return int(value, self.base)
A config value that is an integer.
268 def __init__(self, default: int = 0, base: int = DECIMAL) -> None: # noqa: D107 269 super().__init__(default) 270 self.base = base
Initialize the base data type.
272 @staticmethod 273 def int_to_base(number: int, base: int) -> int: 274 """Convert an integer to a string representation in a given base.""" 275 if number == 0: 276 return 0 277 digits = [] 278 while number: 279 digits.append(str(number % base)) 280 number //= base 281 return int("".join(reversed(digits)))
Convert an integer to a string representation in a given base.
290 def convert(self, value: str) -> int: 291 """Convert a string value to an integer.""" 292 if "c" in value: 293 base_str, val_str = value.split("c") 294 base = int(base_str) 295 if base != self.base: 296 msg = "Base in string does not match base in Integer while converting." 297 raise ValueError(msg) 298 return int(val_str, self.base) 299 return int(value, self.base)
Convert a string value to an integer.
10class InvalidConverterError(ValueError): 11 """Raised when the converter is not set or invalid."""
Raised when the converter is not set or invalid.
6class InvalidDefaultError(ValueError): 7 """Raised when the default value is not set or invalid."""
Raised when the default value is not set or invalid.
468class List(_SequenceType[T], Generic[T]): 469 """A config value that is a list of values.""" 470 471 def convert(self, value: str) -> list[T]: 472 """Convert a string to a list.""" 473 return list(super()._convert(value))
A config value that is a list of values.
200class NoneType(BaseDataType[None]): 201 """A config value that is None.""" 202 203 null_values: ClassVar[set[str]] = {"none", "null", "nil"} 204 205 def __init__(self) -> None: 206 """Initialize the NoneType data type.""" 207 super().__init__(None) 208 209 def is_valid(self, value: str) -> bool: 210 """Check if the provided string value is in the set of null values.""" 211 return value.casefold().strip() in NoneType.null_values 212 213 def convert(self, value: str) -> None: 214 """Convert a string value to None.""" 215 if self.is_valid(value): 216 return 217 msg = f"Value '{value}' is not a valid null value. Expected one of: {', '.join(NoneType.null_values)}." 218 raise ValueError(msg)
A config value that is None.
205 def __init__(self) -> None: 206 """Initialize the NoneType data type.""" 207 super().__init__(None)
Initialize the NoneType data type.
209 def is_valid(self, value: str) -> bool: 210 """Check if the provided string value is in the set of null values.""" 211 return value.casefold().strip() in NoneType.null_values
Check if the provided string value is in the set of null values.
213 def convert(self, value: str) -> None: 214 """Convert a string value to None.""" 215 if self.is_valid(value): 216 return 217 msg = f"Value '{value}' is not a valid null value. Expected one of: {', '.join(NoneType.null_values)}." 218 raise ValueError(msg)
Convert a string value to None.
314class Octal(Integer): 315 """A config value that represents octal.""" 316 317 def __init__(self, default: int = 0, base: int = OCTAL) -> None: # noqa: D107 318 super().__init__(default, base) 319 320 def __str__(self) -> str: # noqa: D105 321 return f"0o{self.value:o}" 322 323 def convert(self, value: str) -> int: 324 """Convert a string value to an integer from octal.""" 325 return int(value.removeprefix("0o"), 8)
A config value that represents octal.
344class Optional(BaseDataType[T | None], Generic[T]): 345 """A config value that is optional, can be None or a specific type.""" 346 347 _none_type = NoneType() 348 349 def __init__(self, data_type: BaseDataType[T]) -> None: 350 """Initialize the optional data type. Wrapping the provided data type.""" 351 self._data_type = data_type 352 353 @property 354 def default(self) -> T | None: 355 """Get the default value of the wrapped data type.""" 356 return self._data_type.default 357 358 @property 359 def value(self) -> T | None: 360 """Get the current value of the wrapped data type.""" 361 return self._data_type.value 362 363 @value.setter 364 def value(self, value: T | None) -> None: 365 """Set the current value of the wrapped data type.""" 366 self._data_type.value = value 367 368 def convert(self, value: str) -> T | None: 369 """Convert a string value to the optional type.""" 370 if self._none_type.is_valid(value): 371 return self._none_type.convert(value) 372 return self._data_type.convert(value) 373 374 def validate(self) -> bool: 375 """Validate that the value is of the wrapped data type or None.""" 376 if self._data_type.value is None: 377 return True 378 return self._data_type.validate() 379 380 def __str__(self) -> str: 381 """Return the string representation of the wrapped data type.""" 382 return str(self._data_type)
A config value that is optional, can be None or a specific type.
349 def __init__(self, data_type: BaseDataType[T]) -> None: 350 """Initialize the optional data type. Wrapping the provided data type.""" 351 self._data_type = data_type
Initialize the optional data type. Wrapping the provided data type.
353 @property 354 def default(self) -> T | None: 355 """Get the default value of the wrapped data type.""" 356 return self._data_type.default
Get the default value of the wrapped data type.
358 @property 359 def value(self) -> T | None: 360 """Get the current value of the wrapped data type.""" 361 return self._data_type.value
Get the current value of the wrapped data type.
368 def convert(self, value: str) -> T | None: 369 """Convert a string value to the optional type.""" 370 if self._none_type.is_valid(value): 371 return self._none_type.convert(value) 372 return self._data_type.convert(value)
Convert a string value to the optional type.
482class Set(BaseDataType[set[T]], Generic[T]): 483 """A config value that is a set of values.""" 484 485 @overload 486 def __init__(self, default: set[T]) -> None: ... 487 @overload 488 def __init__(self, *, data_type: BaseDataType[T]) -> None: ... 489 @overload 490 def __init__( 491 self, 492 default: set[T], 493 *, 494 data_type: BaseDataType[T] = ..., 495 ) -> None: ... 496 497 def __init__(self, default: set[T] = UNSET, *, data_type: BaseDataType[T] = UNSET) -> None: 498 """Initialize the set data type.""" 499 if default is UNSET and data_type is UNSET: 500 msg = "Set requires either a default with at least one element, or data_type to be specified." 501 raise InvalidDefaultError(msg) 502 if default is UNSET: 503 default = set() 504 super().__init__(default) 505 self._infer_type(default, data_type) 506 507 def _infer_type(self, default: set[T], data_type: BaseDataType[T]) -> None: 508 if len(default) <= 0 and data_type is UNSET: 509 msg = "Set default must have at least one element to infer type. or specify `data_type=<BaseDataType>`" 510 raise InvalidDefaultError(msg) 511 if data_type is UNSET: 512 sample_element = default.pop() 513 default.add(sample_element) 514 self._data_type = BaseDataType[T].cast(sample_element) 515 else: 516 self._data_type = data_type 517 518 def convert(self, value: str) -> set[T]: 519 """Convert a string to a set.""" 520 if not value: 521 return set() 522 parts = value.split(",") 523 return {self._data_type.convert(item.strip()) for item in parts} 524 525 def __str__(self) -> str: 526 """Return a string representation of the set.""" 527 return ",".join(str(item) for item in self.value)
A config value that is a set of values.
497 def __init__(self, default: set[T] = UNSET, *, data_type: BaseDataType[T] = UNSET) -> None: 498 """Initialize the set data type.""" 499 if default is UNSET and data_type is UNSET: 500 msg = "Set requires either a default with at least one element, or data_type to be specified." 501 raise InvalidDefaultError(msg) 502 if default is UNSET: 503 default = set() 504 super().__init__(default) 505 self._infer_type(default, data_type)
Initialize the set data type.
147class StrEnum(_EnumBase[StrEnumType]): 148 """A config value that is an enum.""" 149 150 def convert(self, value: str) -> StrEnumType: 151 """Convert a string value to an enum.""" 152 value = self._strip_comment(value) 153 return self.value.__class__(value) 154 155 def _format_allowed_values(self) -> str: 156 """Format allowed values as comma-separated member values.""" 157 enum_class = self.value.__class__ 158 return ", ".join(member.value for member in enum_class) 159 160 def _get_value_str(self) -> str: 161 """Get the member value.""" 162 return self.value.value
A config value that is an enum.
221class String(BaseDataType[str]): 222 """A config value that is a string.""" 223 224 def __init__(self, default: str = "") -> None: # noqa: D107 225 super().__init__(default) 226 227 def convert(self, value: str) -> str: 228 """Convert a string value to a string.""" 229 return value
A config value that is a string.
684class Time(BaseDataType[time]): 685 """A config value that is a time.""" 686 687 @overload 688 def __init__(self, default: time = UNSET) -> None: ... 689 @overload 690 def __init__(self, **kwargs: Unpack[_TimeKwargs]) -> None: ... 691 692 def __init__(self, default: time = UNSET, **kwargs: Unpack[_TimeKwargs]) -> None: 693 """Initialize the time data type. Defaults to current time if not provided.""" 694 if default is UNSET: 695 default = time(**kwargs) 696 super().__init__(default) 697 698 def convert(self, value: str) -> time: 699 """Convert a string value to a time.""" 700 return time.fromisoformat(value) 701 702 def __str__(self) -> str: # noqa: D105 703 return self.value.isoformat()
A config value that is a time.
692 def __init__(self, default: time = UNSET, **kwargs: Unpack[_TimeKwargs]) -> None: 693 """Initialize the time data type. Defaults to current time if not provided.""" 694 if default is UNSET: 695 default = time(**kwargs) 696 super().__init__(default)
Initialize the time data type. Defaults to current time if not provided.
714class TimeDelta(BaseDataType[timedelta]): 715 """A config value that is a timedelta.""" 716 717 def __init__( 718 self, 719 default: timedelta = UNSET, 720 **kwargs: Unpack[_TimeDeltaKwargs], 721 ) -> None: 722 """Initialize the timedelta data type. Defaults to 0 if not provided.""" 723 if default is UNSET: 724 default = timedelta(**kwargs) 725 super().__init__(default) 726 727 def convert(self, value: str) -> timedelta: 728 """Convert a string value to a timedelta.""" 729 return timedelta(seconds=float(value)) 730 731 def __str__(self) -> str: # noqa: D105 732 return str(self.value.total_seconds())
A config value that is a timedelta.
717 def __init__( 718 self, 719 default: timedelta = UNSET, 720 **kwargs: Unpack[_TimeDeltaKwargs], 721 ) -> None: 722 """Initialize the timedelta data type. Defaults to 0 if not provided.""" 723 if default is UNSET: 724 default = timedelta(**kwargs) 725 super().__init__(default)
Initialize the timedelta data type. Defaults to 0 if not provided.
475class Tuple(_SequenceType[T], Generic[T]): 476 """A config value that is a tuple of values.""" 477 478 def convert(self, value: str) -> tuple[T, ...]: 479 """Convert a string to a tuple.""" 480 return tuple(super()._convert(value))
A config value that is a tuple of values.