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