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""" 5 6from .config import Config 7from .data_types import ( 8 BaseDataType, 9 Binary, 10 Boolean, 11 Enum, 12 Float, 13 Hex, 14 Integer, 15 IntEnum, 16 IntFlag, 17 List, 18 NoneType, 19 Octal, 20 Optional, 21 StrEnum, 22 String, 23) 24from .exceptions import InvalidConverterError, InvalidDefaultError 25 26__all__ = [ 27 "BaseDataType", 28 "Binary", 29 "Boolean", 30 "Config", 31 "Enum", 32 "Float", 33 "Hex", 34 "IntEnum", 35 "IntFlag", 36 "Integer", 37 "InvalidConverterError", 38 "InvalidDefaultError", 39 "List", 40 "NoneType", 41 "Octal", 42 "Optional", 43 "StrEnum", 44 "String", 45]
17class BaseDataType(ABC, Generic[T]): 18 """Base class used for Config descriptors to define a data type.""" 19 20 def __init__(self, default: T) -> None: 21 """Initialize the base data type.""" 22 self.default = default 23 self.value = default 24 25 def __str__(self) -> str: 26 """Return the string representation of the stored value.""" 27 return str(self.value) 28 29 @abstractmethod 30 def convert(self, value: str) -> T: 31 """Convert a string value to the desired type.""" 32 33 def validate(self) -> bool: 34 """Validate that the value matches the expected type.""" 35 orig_bases: tuple[type, ...] | None = getattr(self.__class__, "__orig_bases__", None) 36 37 if not orig_bases: 38 msg = "No type information available for validation." 39 raise InvalidConverterError(msg) 40 41 # Extract type arguments from the generic base 42 for base in orig_bases: 43 if hasattr(base, "__args__"): 44 type_args = base.__args__ 45 if type_args: 46 for type_arg in type_args: 47 if hasattr(type_arg, "__origin__"): 48 # For parameterized generics, check against the origin type 49 if isinstance(self.value, type_arg.__origin__): 50 return True 51 elif isinstance(self.value, type_arg): 52 return True 53 msg = f"Value {self.value} is not any of {type_args}." 54 raise InvalidConverterError(msg) 55 msg = "This should not have raised. Report to the library maintainers with code: `DTBDT`" 56 raise TypeError(msg) 57 58 @staticmethod 59 def cast_optional(default: T | None | BaseDataType[T]) -> BaseDataType[T | None]: 60 """Convert the default value to an Optional data type.""" 61 if default is None: 62 return cast("BaseDataType[T | None]", NoneType()) 63 return Optional(BaseDataType.cast(default)) 64 65 @staticmethod 66 def cast(default: T | BaseDataType[T]) -> BaseDataType[T]: 67 """Convert the default value to a BaseDataType.""" 68 # We use Cast to shut up type checkers, as we know primitive types will be correct. 69 # If a custom type is passed, it should be a BaseDataType subclass, which already has the correct types. 70 match default: 71 case bool(): 72 data_type = cast("BaseDataType[T]", Boolean(default)) 73 case None: 74 data_type = cast("BaseDataType[T]", NoneType()) 75 case int(): 76 data_type = cast("BaseDataType[T]", Integer(default)) 77 case float(): 78 data_type = cast("BaseDataType[T]", Float(default)) 79 case str(): 80 data_type = cast("BaseDataType[T]", String(default)) 81 case BaseDataType(): 82 data_type = default 83 case _: 84 msg = ( 85 f"Unsupported default value type: {type(default).__name__}. " 86 "Use a BaseDataType subclass for custom types." 87 ) 88 raise InvalidDefaultError(msg) 89 return data_type
Base class used for Config descriptors to define a data type.
20 def __init__(self, default: T) -> None: 21 """Initialize the base data type.""" 22 self.default = default 23 self.value = default
Initialize the base data type.
29 @abstractmethod 30 def convert(self, value: str) -> T: 31 """Convert a string value to the desired type."""
Convert a string value to the desired type.
33 def validate(self) -> bool: 34 """Validate that the value matches the expected type.""" 35 orig_bases: tuple[type, ...] | None = getattr(self.__class__, "__orig_bases__", None) 36 37 if not orig_bases: 38 msg = "No type information available for validation." 39 raise InvalidConverterError(msg) 40 41 # Extract type arguments from the generic base 42 for base in orig_bases: 43 if hasattr(base, "__args__"): 44 type_args = base.__args__ 45 if type_args: 46 for type_arg in type_args: 47 if hasattr(type_arg, "__origin__"): 48 # For parameterized generics, check against the origin type 49 if isinstance(self.value, type_arg.__origin__): 50 return True 51 elif isinstance(self.value, type_arg): 52 return True 53 msg = f"Value {self.value} is not any of {type_args}." 54 raise InvalidConverterError(msg) 55 msg = "This should not have raised. Report to the library maintainers with code: `DTBDT`" 56 raise TypeError(msg)
Validate that the value matches the expected type.
58 @staticmethod 59 def cast_optional(default: T | None | BaseDataType[T]) -> BaseDataType[T | None]: 60 """Convert the default value to an Optional data type.""" 61 if default is None: 62 return cast("BaseDataType[T | None]", NoneType()) 63 return Optional(BaseDataType.cast(default))
Convert the default value to an Optional data type.
65 @staticmethod 66 def cast(default: T | BaseDataType[T]) -> BaseDataType[T]: 67 """Convert the default value to a BaseDataType.""" 68 # We use Cast to shut up type checkers, as we know primitive types will be correct. 69 # If a custom type is passed, it should be a BaseDataType subclass, which already has the correct types. 70 match default: 71 case bool(): 72 data_type = cast("BaseDataType[T]", Boolean(default)) 73 case None: 74 data_type = cast("BaseDataType[T]", NoneType()) 75 case int(): 76 data_type = cast("BaseDataType[T]", Integer(default)) 77 case float(): 78 data_type = cast("BaseDataType[T]", Float(default)) 79 case str(): 80 data_type = cast("BaseDataType[T]", String(default)) 81 case BaseDataType(): 82 data_type = default 83 case _: 84 msg = ( 85 f"Unsupported default value type: {type(default).__name__}. " 86 "Use a BaseDataType subclass for custom types." 87 ) 88 raise InvalidDefaultError(msg) 89 return data_type
Convert the default value to a BaseDataType.
242class Binary(BaseDataType[bytes | int]): 243 """A config value that represents binary.""" 244 245 def __init__(self, default: bytes | int) -> None: # noqa: D107 246 if isinstance(default, bytes): 247 default = int.from_bytes(default) 248 super().__init__(default) 249 250 def __str__(self) -> str: # noqa: D105 251 if isinstance(self.value, bytes): 252 self.value = int.from_bytes(self.value) 253 return f"0b{self.value:b}" 254 255 def convert(self, value: str) -> int: 256 """Convert a string value to an integer from binary.""" 257 return int(value.removeprefix("0b"), 2)
A config value that represents binary.
158class Boolean(BaseDataType[bool]): 159 """A config value that is a boolean.""" 160 161 def __init__(self, default: bool = False) -> None: # noqa: D107, FBT001, FBT002 162 super().__init__(default) 163 164 def convert(self, value: str) -> bool: 165 """Convert a string value to a boolean.""" 166 if value.lower() in {"true", "1", "yes"}: 167 return True 168 if value.lower() in {"false", "0", "no"}: 169 return False 170 msg = f"Cannot convert {value} to boolean." 171 raise ValueError(msg)
A config value that is a boolean.
161 def __init__(self, default: bool = False) -> None: # noqa: D107, FBT001, FBT002 162 super().__init__(default)
Initialize the base data type.
164 def convert(self, value: str) -> bool: 165 """Convert a string value to a boolean.""" 166 if value.lower() in {"true", "1", "yes"}: 167 return True 168 if value.lower() in {"false", "0", "no"}: 169 return False 170 msg = f"Cannot convert {value} to boolean." 171 raise ValueError(msg)
Convert a string value to a boolean.
29class Config(Generic[VT]): 30 """A descriptor for config values, preserving type information. 31 32 the ValueType (VT) is the type you want the config value to be. 33 """ 34 35 validate_types: ClassVar[bool] = True # Validate that the converter returns the same type as the default value. (not strict) 36 write_on_edit: ClassVar[bool] = True # Write to the config file when updating a value. 37 optional: bool = False # if True, allows None as an extra type when validating types. (both instance and class variables.) 38 39 _parser: ConfigParser = UNSET 40 _file: Path = UNSET 41 _has_read_config: bool = False 42 43 if TYPE_CHECKING: 44 # Overloads for type checkers to understand the different settings of the Config descriptors. 45 @overload 46 def __init__(self: Config[str], default: str) -> None: ... 47 @overload 48 def __init__(self: Config[None], default: None) -> None: ... 49 @overload 50 def __init__(self: Config[bool], default: bool) -> None: ... # noqa: FBT001 51 @overload 52 def __init__(self: Config[int], default: int) -> None: ... 53 @overload 54 def __init__(self: Config[float], default: float) -> None: ... 55 @overload 56 def __init__(self: Config[str | None], default: str, *, optional: bool) -> None: ... 57 @overload 58 def __init__(self: Config[None], default: None, *, optional: bool) -> None: ... 59 @overload 60 def __init__(self: Config[bool | None], default: bool, *, optional: bool) -> None: ... # noqa: FBT001 61 @overload 62 def __init__(self: Config[int | None], default: int, *, optional: bool) -> None: ... 63 @overload 64 def __init__(self: Config[float | None], default: float, *, optional: bool) -> None: ... 65 @overload # Custom data type, like Enum's or custom class. 66 def __init__(self, default: BaseDataType[VT]) -> None: ... 67 68 # type Complains about the self and default overloads for None and str 69 # they are explicitly set for type checkers, the actual representation doesn't matter 70 # in runtime, as VT is allowed to be any type. 71 def __init__( # type: ignore[reportInconsistentOverload] 72 self, 73 default: VT | None | BaseDataType[VT] = UNSET, 74 *, 75 optional: bool = False, 76 ) -> None: 77 """Initialize the config descriptor with a default value. 78 79 Validate that parser and filepath are present. 80 """ 81 self.optional = optional or Config.optional # Be truthy when either one is true. 82 83 if not self.optional and default is UNSET: 84 msg = "Default value cannot be None when optional is False." 85 raise InvalidDefaultError(msg) 86 87 self._initialize_data_type(default) 88 self._validate_init() 89 self._read_parser() 90 91 def _initialize_data_type(self, default: VT | None | BaseDataType[VT]) -> None: 92 """Initialize the data type based on the default value.""" 93 if not self.optional and default is not None: 94 self._data_type = BaseDataType[VT].cast(default) 95 else: 96 self._data_type = BaseDataType[VT].cast_optional(default) 97 98 def _read_parser(self) -> None: 99 """Ensure the parser has read the file at initialization. Avoids rewriting the file when settings are already set.""" 100 if not self._has_read_config: 101 Config._parser.read(Config._file) 102 Config._has_read_config = True 103 104 def _validate_init(self) -> None: 105 """Validate the config descriptor, ensuring it's properly set up.""" 106 self.validate_file() 107 self.validate_parser() 108 109 def convert(self, value: str) -> VT: 110 """Convert the value to the desired type using the given converter method.""" 111 # Ignore the type error of VT, type checkers don't like None as an option 112 # We handle it using the `optional` flag, or using Optional DataType. so we can safely ignore it. 113 return self._data_type.convert(value) # type: ignore[reportReturnType] 114 115 @staticmethod 116 def set_parser(parser: ConfigParser) -> None: 117 """Set the parser for ALL descriptors.""" 118 Config._parser = parser 119 120 @staticmethod 121 def set_file(file: Path) -> None: 122 """Set the file for ALL descriptors.""" 123 Config._file = file 124 125 def validate_strict_type(self) -> None: 126 """Validate the type of the converter matches the desired type.""" 127 if self._data_type.convert is UNSET: 128 msg = "Converter is not set." 129 raise InvalidConverterError(msg) 130 131 self.__config_value = Config._parser.get(self._section, self._setting) 132 self.__converted_value = self.convert(self.__config_value) 133 134 if not Config.validate_types: 135 return 136 if not self._data_type.validate(): 137 msg = f"Invalid value for {self._section}.{self._setting}: {self.__converted_value}" 138 raise InvalidConverterError(msg) 139 140 self.__converted_type = type(self.__converted_value) 141 default_value_type = type(self._data_type.default) 142 143 is_optional = self.optional or isinstance(self._data_type, Optional) 144 if (is_optional) and self.__converted_type in (default_value_type, NoneType): 145 # Allow None or the same type as the default value to be returned by the converter when _optional is True. 146 return 147 if self.__converted_type is not default_value_type: 148 msg = f"Converter does not return the same type as the default value <{default_value_type}> got <{self.__converted_type}>." # noqa: E501 149 raise InvalidConverterError(msg) 150 151 @staticmethod 152 def validate_file() -> None: 153 """Validate the config file.""" 154 if Config._file is UNSET: 155 msg = f"Config file is not set. use {Config.__name__}.set_file() to set it." 156 raise ValueError(msg) 157 158 @staticmethod 159 def validate_parser() -> None: 160 """Validate the config parser.""" 161 if Config._parser is UNSET: 162 msg = f"Config parser is not set. use {Config.__name__}.set_parser() to set it." 163 raise ValueError(msg) 164 165 def __set_name__(self, owner: type, name: str) -> None: 166 """Set the name of the attribute to the name of the descriptor.""" 167 self.name = name 168 self._section = owner.__name__ 169 self._setting = name 170 self._ensure_option() 171 self._original_value = Config._parser.get(self._section, self._setting) or self._data_type.default 172 self.private = f"_{self._section}_{self._setting}_{self.name}" 173 174 def _ensure_section(self) -> None: 175 """Ensure the section exists in the config file. Creates one if it doesn't exist.""" 176 if not self._parser.has_section(self._section): 177 self._parser.add_section(self._section) 178 179 def _ensure_option(self) -> None: 180 """Ensure the option exists in the config file. Creates one if it doesn't exist.""" 181 self._ensure_section() 182 if not self._parser.has_option(self._section, self._setting): 183 Config._set(self._section, self._setting, str(self._data_type)) 184 185 def __get__(self, obj: object, obj_type: object) -> VT: 186 """Get the value of the attribute.""" 187 # obj_type is the class in which the variable is defined 188 # so it can be different than type of VT 189 # but we don't need obj or it's type to get the value from config in our case. 190 self.validate_strict_type() 191 return self.__converted_value 192 193 def __set__(self, obj: object, value: VT) -> None: 194 """Set the value of the attribute.""" 195 self._data_type.value = value 196 Config._set(self._section, self._setting, str(self._data_type)) 197 setattr(obj, self.private, value) 198 199 @staticmethod 200 def _sanitize_str(value: str) -> str: 201 """Escape the percent sign in the value.""" 202 return value.replace("%", "%%") 203 204 @staticmethod 205 def _set(section: str, setting: str, value: VT) -> None: 206 """Set a config value, and write it to the file.""" 207 if not Config._parser.has_section(section): 208 Config._parser.add_section(section) 209 sanitized_str = Config._sanitize_str(str(value)) 210 Config._parser.set(section, setting, sanitized_str) 211 if Config.write_on_edit: 212 Config.write() 213 214 @staticmethod 215 def write() -> None: 216 """Write the config parser to the file.""" 217 Config.validate_file() 218 with Config._file.open("w") as f: 219 Config._parser.write(f) 220 221 @staticmethod 222 def set(section: str, setting: str, value: VT): # noqa: ANN205 223 """Set a config value using this descriptor.""" 224 225 def wrapper(func: Callable[..., F]) -> Callable[..., F]: 226 @wraps(func) 227 def inner(*args: P.args, **kwargs: P.kwargs) -> F: 228 Config._set(section, setting, value) 229 return func(*args, **kwargs) 230 231 return inner 232 return wrapper 233 234 @staticmethod 235 def with_setting(setting: Config[OVT]): # noqa: ANN205 236 """Insert a config value into **kwargs to a given method/function using this decorator.""" 237 def wrapper(func: Callable[..., F]) -> Callable[..., F]: 238 @wraps(func) 239 def inner(*args: P.args, **kwargs: P.kwargs) -> F: 240 kwargs[setting.name] = setting.convert(Config._parser.get(setting._section, setting._setting)) 241 return func(*args, **kwargs) 242 243 return inner 244 return wrapper 245 246 @staticmethod 247 def with_kwarg(section: str, setting: str, name: str | None = None, default: VT = UNSET): # noqa: ANN205 248 """Insert a config value into **kwargs to a given method/function using this descriptor. 249 250 Use kwarg.get(`name`) to get the value. 251 `name` is the name the kwarg gets if passed, if None, it will be the same as `setting`. 252 Section parameter is just for finding the config value. 253 """ 254 if name is None: 255 name = setting 256 if default is UNSET and not Config._parser.has_option(section, setting): 257 msg = f"Config value {section=} {setting=} is not set. and no default value is given." 258 raise ValueError(msg) 259 260 def wrapper(func: Callable[..., F]) -> Callable[..., F]: 261 @wraps(func) 262 def inner(*args: P.args, **kwargs: P.kwargs) -> F: 263 if default is not UNSET: 264 Config._set_default(section, setting, default) 265 kwargs[name] = Config._parser.get(section, setting) # ty: ignore[invalid-assignment] 266 return func(*args, **kwargs) 267 268 return inner 269 return wrapper 270 271 @staticmethod 272 def _set_default(section: str, setting: str, value: VT) -> None: 273 if Config._parser.get(section, setting, fallback=UNSET) is UNSET: 274 Config._set(section, setting, value) 275 276 @staticmethod 277 def default(section: str, setting: str, value: VT): # noqa: ANN205 278 """Set a default config value if none are set yet using this descriptor.""" 279 def wrapper(func: Callable[..., F]) -> Callable[..., F]: 280 @wraps(func) 281 def inner(*args: P.args, **kwargs: P.kwargs) -> F: 282 Config._set_default(section, setting, value) 283 return func(*args, **kwargs) 284 285 return inner 286 return wrapper
A descriptor for config values, preserving type information.
the ValueType (VT) is the type you want the config value to be.
71 def __init__( # type: ignore[reportInconsistentOverload] 72 self, 73 default: VT | None | BaseDataType[VT] = UNSET, 74 *, 75 optional: bool = False, 76 ) -> None: 77 """Initialize the config descriptor with a default value. 78 79 Validate that parser and filepath are present. 80 """ 81 self.optional = optional or Config.optional # Be truthy when either one is true. 82 83 if not self.optional and default is UNSET: 84 msg = "Default value cannot be None when optional is False." 85 raise InvalidDefaultError(msg) 86 87 self._initialize_data_type(default) 88 self._validate_init() 89 self._read_parser()
Initialize the config descriptor with a default value.
Validate that parser and filepath are present.
109 def convert(self, value: str) -> VT: 110 """Convert the value to the desired type using the given converter method.""" 111 # Ignore the type error of VT, type checkers don't like None as an option 112 # We handle it using the `optional` flag, or using Optional DataType. so we can safely ignore it. 113 return self._data_type.convert(value) # type: ignore[reportReturnType]
Convert the value to the desired type using the given converter method.
115 @staticmethod 116 def set_parser(parser: ConfigParser) -> None: 117 """Set the parser for ALL descriptors.""" 118 Config._parser = parser
Set the parser for ALL descriptors.
120 @staticmethod 121 def set_file(file: Path) -> None: 122 """Set the file for ALL descriptors.""" 123 Config._file = file
Set the file for ALL descriptors.
125 def validate_strict_type(self) -> None: 126 """Validate the type of the converter matches the desired type.""" 127 if self._data_type.convert is UNSET: 128 msg = "Converter is not set." 129 raise InvalidConverterError(msg) 130 131 self.__config_value = Config._parser.get(self._section, self._setting) 132 self.__converted_value = self.convert(self.__config_value) 133 134 if not Config.validate_types: 135 return 136 if not self._data_type.validate(): 137 msg = f"Invalid value for {self._section}.{self._setting}: {self.__converted_value}" 138 raise InvalidConverterError(msg) 139 140 self.__converted_type = type(self.__converted_value) 141 default_value_type = type(self._data_type.default) 142 143 is_optional = self.optional or isinstance(self._data_type, Optional) 144 if (is_optional) and self.__converted_type in (default_value_type, NoneType): 145 # Allow None or the same type as the default value to be returned by the converter when _optional is True. 146 return 147 if self.__converted_type is not default_value_type: 148 msg = f"Converter does not return the same type as the default value <{default_value_type}> got <{self.__converted_type}>." # noqa: E501 149 raise InvalidConverterError(msg)
Validate the type of the converter matches the desired type.
151 @staticmethod 152 def validate_file() -> None: 153 """Validate the config file.""" 154 if Config._file is UNSET: 155 msg = f"Config file is not set. use {Config.__name__}.set_file() to set it." 156 raise ValueError(msg)
Validate the config file.
158 @staticmethod 159 def validate_parser() -> None: 160 """Validate the config parser.""" 161 if Config._parser is UNSET: 162 msg = f"Config parser is not set. use {Config.__name__}.set_parser() to set it." 163 raise ValueError(msg)
Validate the config parser.
214 @staticmethod 215 def write() -> None: 216 """Write the config parser to the file.""" 217 Config.validate_file() 218 with Config._file.open("w") as f: 219 Config._parser.write(f)
Write the config parser to the file.
221 @staticmethod 222 def set(section: str, setting: str, value: VT): # noqa: ANN205 223 """Set a config value using this descriptor.""" 224 225 def wrapper(func: Callable[..., F]) -> Callable[..., F]: 226 @wraps(func) 227 def inner(*args: P.args, **kwargs: P.kwargs) -> F: 228 Config._set(section, setting, value) 229 return func(*args, **kwargs) 230 231 return inner 232 return wrapper
Set a config value using this descriptor.
234 @staticmethod 235 def with_setting(setting: Config[OVT]): # noqa: ANN205 236 """Insert a config value into **kwargs to a given method/function using this decorator.""" 237 def wrapper(func: Callable[..., F]) -> Callable[..., F]: 238 @wraps(func) 239 def inner(*args: P.args, **kwargs: P.kwargs) -> F: 240 kwargs[setting.name] = setting.convert(Config._parser.get(setting._section, setting._setting)) 241 return func(*args, **kwargs) 242 243 return inner 244 return wrapper
Insert a config value into **kwargs to a given method/function using this decorator.
246 @staticmethod 247 def with_kwarg(section: str, setting: str, name: str | None = None, default: VT = UNSET): # noqa: ANN205 248 """Insert a config value into **kwargs to a given method/function using this descriptor. 249 250 Use kwarg.get(`name`) to get the value. 251 `name` is the name the kwarg gets if passed, if None, it will be the same as `setting`. 252 Section parameter is just for finding the config value. 253 """ 254 if name is None: 255 name = setting 256 if default is UNSET and not Config._parser.has_option(section, setting): 257 msg = f"Config value {section=} {setting=} is not set. and no default value is given." 258 raise ValueError(msg) 259 260 def wrapper(func: Callable[..., F]) -> Callable[..., F]: 261 @wraps(func) 262 def inner(*args: P.args, **kwargs: P.kwargs) -> F: 263 if default is not UNSET: 264 Config._set_default(section, setting, default) 265 kwargs[name] = Config._parser.get(section, setting) # ty: ignore[invalid-assignment] 266 return func(*args, **kwargs) 267 268 return inner 269 return wrapper
Insert a config value into **kwargs to a given 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.
276 @staticmethod 277 def default(section: str, setting: str, value: VT): # noqa: ANN205 278 """Set a default config value if none are set yet using this descriptor.""" 279 def wrapper(func: Callable[..., F]) -> Callable[..., F]: 280 @wraps(func) 281 def inner(*args: P.args, **kwargs: P.kwargs) -> F: 282 Config._set_default(section, setting, value) 283 return func(*args, **kwargs) 284 285 return inner 286 return wrapper
Set a default config value if none are set yet using this descriptor.
91class Enum(BaseDataType[enum.Enum]): 92 """A config value that is an enum.""" 93 94 def convert(self, value: str) -> enum.Enum: 95 """Convert a string value to an enum.""" 96 parsed_enum_name = value.split(".")[-1] 97 return self.value.__class__[parsed_enum_name]
A config value that is an enum.
147class Float(BaseDataType[float]): 148 """A config value that is a float.""" 149 150 def __init__(self, default: float = 0.0) -> None: # noqa: D107 151 super().__init__(default) 152 153 def convert(self, value: str) -> float: 154 """Convert a string value to a float.""" 155 return float(value)
A config value that is a float.
216class Hex(Integer): 217 """A config value that represents hexadecimal.""" 218 219 def __init__(self, default: int, base: int = HEXADECIMAL) -> None: # noqa: D107 220 super().__init__(default, base) 221 222 def __str__(self) -> str: # noqa: D105 223 return f"0x{self.value:x}" 224 225 def convert(self, value: str) -> int: 226 """Convert a string value to an integer. from hexadecimal.""" 227 return int(value.removeprefix("0x"), 16)
A config value that represents hexadecimal.
106class IntEnum(BaseDataType[enum.IntEnum]): 107 """A config value that is an enum.""" 108 109 def convert(self, value: str) -> enum.IntEnum: 110 """Convert a string value to an enum.""" 111 return self.value.__class__(int(value))
A config value that is an enum.
113class IntFlag(BaseDataType[enum.IntFlag]): 114 """A config value that is an enum.""" 115 116 def convert(self, value: str) -> enum.IntFlag: 117 """Convert a string value to an enum.""" 118 return self.value.__class__(int(value))
A config value that is an enum.
178class Integer(BaseDataType[int]): 179 """A config value that is an integer.""" 180 181 # Define constants for common bases 182 183 def __init__(self, default: int = 0, base: int = DECIMAL) -> None: # noqa: D107 184 super().__init__(default) 185 self.base = base 186 187 @staticmethod 188 def int_to_base(number: int, base: int) -> int: 189 """Convert an integer to a string representation in a given base.""" 190 if number == 0: 191 return 0 192 digits = [] 193 while number: 194 digits.append(str(number % base)) 195 number //= base 196 return int("".join(reversed(digits))) 197 198 def __str__(self) -> str: # noqa: D105 199 if self.base == DECIMAL: 200 return str(self.value) 201 # Convert the base 10 int to base 5 202 self.value = self.int_to_base(int(self.value), self.base) 203 return f"{self.base}c{self.value}" 204 205 def convert(self, value: str) -> int: 206 """Convert a string value to an integer.""" 207 if "c" in value: 208 base_str, val_str = value.split("c") 209 base = int(base_str) 210 if base != self.base: 211 msg = "Base in string does not match base in Integer while converting." 212 raise ValueError(msg) 213 return int(val_str, self.base) 214 return int(value, self.base)
A config value that is an integer.
183 def __init__(self, default: int = 0, base: int = DECIMAL) -> None: # noqa: D107 184 super().__init__(default) 185 self.base = base
Initialize the base data type.
187 @staticmethod 188 def int_to_base(number: int, base: int) -> int: 189 """Convert an integer to a string representation in a given base.""" 190 if number == 0: 191 return 0 192 digits = [] 193 while number: 194 digits.append(str(number % base)) 195 number //= base 196 return int("".join(reversed(digits)))
Convert an integer to a string representation in a given base.
205 def convert(self, value: str) -> int: 206 """Convert a string value to an integer.""" 207 if "c" in value: 208 base_str, val_str = value.split("c") 209 base = int(base_str) 210 if base != self.base: 211 msg = "Base in string does not match base in Integer while converting." 212 raise ValueError(msg) 213 return int(val_str, self.base) 214 return int(value, self.base)
Convert a string value to an integer.
Raised when the converter is not set or invalid.
4class InvalidDefaultError(ValueError): 5 """Raised when the default value is not set or invalid."""
Raised when the default value is not set or invalid.
295class List(BaseDataType[list[T]], Generic[T]): 296 """A config value that is a list of values.""" 297 298 separator = "," 299 escape_char = "\\" 300 301 def __init__(self, default: list[T], *, data_type: BaseDataType[T] = UNSET) -> None: 302 """Initialize the list data type.""" 303 super().__init__(default) 304 if len(default) <= 0 and data_type is UNSET: 305 msg = "List default must have at least one element to infer type. or specify `data_type=<BaseDataType>`" 306 raise InvalidDefaultError(msg) 307 if data_type is UNSET: 308 self._data_type = BaseDataType[T].cast(default[0]) 309 else: 310 self._data_type = data_type 311 312 def convert(self, value: str) -> list[T]: 313 """Convert a string to a list.""" 314 # Handle empty string as empty list 315 if not value: 316 return [] 317 318 # Split string but respect escaped separators 319 result: list[T] = [] 320 current = "" 321 i = 0 322 while i < len(value): 323 # Check for escaped separator 324 if i < len(value) - 1 and value[i] == self.escape_char and value[i + 1] == self.separator: 325 current += self.separator 326 i += 2 # Skip both the escape char and the separator 327 # Check for escaped escape char 328 elif i < len(value) - 1 and value[i] == self.escape_char and value[i + 1] == self.escape_char: 329 current += self.escape_char 330 i += 2 # Skip both escape chars 331 # Handle separator 332 elif value[i] == self.separator: 333 c = self._data_type.convert(current) 334 result.append(c) 335 current = "" 336 i += 1 337 # Handle regular character 338 else: 339 current += value[i] 340 i += 1 341 342 # Add the last element 343 result.append(self._data_type.convert(current)) 344 345 return result 346 347 def __str__(self) -> str: 348 """Return a string representation of the list.""" 349 values: list[str] = [] 350 for item in self.value: 351 # Escape escape char 352 escaped_item = str(item).replace(self.escape_char, self.escape_char*2) 353 # Escape separator 354 escaped_item = escaped_item.replace(self.separator, f"{self.escape_char}{self.separator}") 355 values.append(escaped_item) 356 357 return self.separator.join(values)
A config value that is a list of values.
301 def __init__(self, default: list[T], *, data_type: BaseDataType[T] = UNSET) -> None: 302 """Initialize the list data type.""" 303 super().__init__(default) 304 if len(default) <= 0 and data_type is UNSET: 305 msg = "List default must have at least one element to infer type. or specify `data_type=<BaseDataType>`" 306 raise InvalidDefaultError(msg) 307 if data_type is UNSET: 308 self._data_type = BaseDataType[T].cast(default[0]) 309 else: 310 self._data_type = data_type
Initialize the list data type.
312 def convert(self, value: str) -> list[T]: 313 """Convert a string to a list.""" 314 # Handle empty string as empty list 315 if not value: 316 return [] 317 318 # Split string but respect escaped separators 319 result: list[T] = [] 320 current = "" 321 i = 0 322 while i < len(value): 323 # Check for escaped separator 324 if i < len(value) - 1 and value[i] == self.escape_char and value[i + 1] == self.separator: 325 current += self.separator 326 i += 2 # Skip both the escape char and the separator 327 # Check for escaped escape char 328 elif i < len(value) - 1 and value[i] == self.escape_char and value[i + 1] == self.escape_char: 329 current += self.escape_char 330 i += 2 # Skip both escape chars 331 # Handle separator 332 elif value[i] == self.separator: 333 c = self._data_type.convert(current) 334 result.append(c) 335 current = "" 336 i += 1 337 # Handle regular character 338 else: 339 current += value[i] 340 i += 1 341 342 # Add the last element 343 result.append(self._data_type.convert(current)) 344 345 return result
Convert a string to a list.
120class NoneType(BaseDataType[None]): 121 """A config value that is None.""" 122 123 null_values: ClassVar[set[str]] = {"none", "null", "nil"} 124 125 def __init__(self) -> None: 126 """Initialize the NoneType data type.""" 127 super().__init__(None) 128 129 def convert(self, value: str) -> bool: # type: ignore[reportIncompatibleMethodOverride] 130 """Convert a string value to None.""" 131 # Ignore type exception as convert should return True/False for NoneType 132 # to determine if we have a valid null value or not. 133 return value.casefold().strip() in NoneType.null_values
A config value that is None.
125 def __init__(self) -> None: 126 """Initialize the NoneType data type.""" 127 super().__init__(None)
Initialize the NoneType data type.
129 def convert(self, value: str) -> bool: # type: ignore[reportIncompatibleMethodOverride] 130 """Convert a string value to None.""" 131 # Ignore type exception as convert should return True/False for NoneType 132 # to determine if we have a valid null value or not. 133 return value.casefold().strip() in NoneType.null_values
Convert a string value to None.
229class Octal(Integer): 230 """A config value that represents octal.""" 231 232 def __init__(self, default: int, base: int = OCTAL) -> None: # noqa: D107 233 super().__init__(default, base) 234 235 def __str__(self) -> str: # noqa: D105 236 return f"0o{self.value:o}" 237 238 def convert(self, value: str) -> int: 239 """Convert a string value to an integer from octal.""" 240 return int(value.removeprefix("0o"), 8)
A config value that represents octal.
259class Optional(BaseDataType[T | None], Generic[T]): 260 """A config value that is optional, can be None or a specific type.""" 261 262 _none_type = NoneType() 263 264 def __init__(self, data_type: BaseDataType[T]) -> None: 265 """Initialize the optional data type. Wrapping the provided data type.""" 266 self._data_type = data_type 267 268 @property 269 def default(self) -> T | None: 270 """Get the default value of the wrapped data type.""" 271 return self._data_type.default 272 273 @property 274 def value(self) -> T | None: 275 """Get the current value of the wrapped data type.""" 276 return self._data_type.value 277 278 @value.setter 279 def value(self, value: T | None) -> None: 280 """Set the current value of the wrapped data type.""" 281 self._data_type.value = value # type: ignore[reportAttributeAccessIssue] 282 283 def convert(self, value: str) -> T | None: 284 """Convert a string value to the optional type.""" 285 if self._none_type.convert(value): 286 return None 287 return self._data_type.convert(value) 288 289 def validate(self) -> bool: 290 """Validate that the value is of the wrapped data type or None.""" 291 if self._data_type.value is None: 292 return True 293 return self._data_type.validate()
A config value that is optional, can be None or a specific type.
264 def __init__(self, data_type: BaseDataType[T]) -> None: 265 """Initialize the optional data type. Wrapping the provided data type.""" 266 self._data_type = data_type
Initialize the optional data type. Wrapping the provided data type.
268 @property 269 def default(self) -> T | None: 270 """Get the default value of the wrapped data type.""" 271 return self._data_type.default
Get the default value of the wrapped data type.
273 @property 274 def value(self) -> T | None: 275 """Get the current value of the wrapped data type.""" 276 return self._data_type.value
Get the current value of the wrapped data type.
99class StrEnum(BaseDataType[enum.StrEnum]): 100 """A config value that is an enum.""" 101 102 def convert(self, value: str) -> enum.StrEnum: 103 """Convert a string value to an enum.""" 104 return self.value.__class__(value)
A config value that is an enum.
136class String(BaseDataType[str]): 137 """A config value that is a string.""" 138 139 def __init__(self, default: str = "") -> None: # noqa: D107 140 super().__init__(default) 141 142 def convert(self, value: str) -> str: 143 """Convert a string value to a string.""" 144 return value
A config value that is a string.