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]
class BaseDataType(abc.ABC, typing.Generic[~T]):
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.

BaseDataType(default: ~T)
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.

default
value
@abstractmethod
def convert(self, value: str) -> ~T:
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.

def validate(self) -> bool:
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.

@staticmethod
def cast_optional( default: Union[~T, NoneType, BaseDataType[~T]]) -> BaseDataType[typing.Optional[~T]]:
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.

@staticmethod
def cast( default: Union[~T, BaseDataType[~T]]) -> BaseDataType[~T]:
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.

class Binary(confkit.BaseDataType[bytes | int]):
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.

Binary(default: bytes | int)
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)

Initialize the base data type.

def convert(self, value: str) -> int:
255    def convert(self, value: str) -> int:
256        """Convert a string value to an integer from binary."""
257        return int(value.removeprefix("0b"), 2)

Convert a string value to an integer from binary.

class Boolean(confkit.BaseDataType[bool]):
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.

Boolean(default: bool = False)
161    def __init__(self, default: bool = False) -> None:  # noqa: D107, FBT001, FBT002
162        super().__init__(default)

Initialize the base data type.

def convert(self, value: str) -> bool:
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.

class Config(typing.Generic[~VT]):
 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.

Config( default: Union[~VT, NoneType, BaseDataType[~VT]] = MISSING, *, optional: bool = False)
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.

validate_types: ClassVar[bool] = True
write_on_edit: ClassVar[bool] = True
optional: bool = False
def convert(self, value: str) -> ~VT:
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.

@staticmethod
def set_parser(parser: configparser.ConfigParser) -> None:
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.

@staticmethod
def set_file(file: pathlib.Path) -> None:
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.

def validate_strict_type(self) -> None:
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.

@staticmethod
def validate_file() -> None:
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.

@staticmethod
def validate_parser() -> None:
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.

@staticmethod
def write() -> None:
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.

@staticmethod
def set(section: str, setting: str, value: ~VT):
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.

@staticmethod
def with_setting(setting: Config[~OVT]):
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.

@staticmethod
def with_kwarg( section: str, setting: str, name: str | None = None, default: ~VT = MISSING):
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.

@staticmethod
def default(section: str, setting: str, value: ~VT):
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.

class Enum(confkit.BaseDataType[enum.Enum]):
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.

def convert(self, value: str) -> enum.Enum:
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]

Convert a string value to an enum.

class Float(confkit.BaseDataType[float]):
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.

Float(default: float = 0.0)
150    def __init__(self, default: float = 0.0) -> None:  # noqa: D107
151        super().__init__(default)

Initialize the base data type.

def convert(self, value: str) -> float:
153    def convert(self, value: str) -> float:
154        """Convert a string value to a float."""
155        return float(value)

Convert a string value to a float.

class Hex(confkit.BaseDataType[int]):
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.

Hex(default: int, base: int = 16)
219    def __init__(self, default: int, base: int = HEXADECIMAL) -> None:  # noqa: D107
220        super().__init__(default, base)

Initialize the base data type.

def convert(self, value: str) -> int:
225    def convert(self, value: str) -> int:
226        """Convert a string value to an integer. from hexadecimal."""
227        return int(value.removeprefix("0x"), 16)

Convert a string value to an integer. from hexadecimal.

class IntEnum(confkit.BaseDataType[enum.IntEnum]):
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.

def convert(self, value: str) -> enum.IntEnum:
109    def convert(self, value: str) -> enum.IntEnum:
110        """Convert a string value to an enum."""
111        return self.value.__class__(int(value))

Convert a string value to an enum.

class IntFlag(confkit.BaseDataType[enum.IntFlag]):
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.

def convert(self, value: str) -> enum.IntFlag:
116    def convert(self, value: str) -> enum.IntFlag:
117        """Convert a string value to an enum."""
118        return self.value.__class__(int(value))

Convert a string value to an enum.

class Integer(confkit.BaseDataType[int]):
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.

Integer(default: int = 0, base: int = 10)
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.

base
@staticmethod
def int_to_base(number: int, base: int) -> int:
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.

def convert(self, value: str) -> int:
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.

class InvalidConverterError(builtins.ValueError):
8class InvalidConverterError(ValueError):
9    """Raised when the converter is not set or invalid."""

Raised when the converter is not set or invalid.

class InvalidDefaultError(builtins.ValueError):
4class InvalidDefaultError(ValueError):
5    """Raised when the default value is not set or invalid."""

Raised when the default value is not set or invalid.

class List(confkit.BaseDataType[list[~T]], typing.Generic[~T]):
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.

List( default: list[~T], *, data_type: BaseDataType[~T] = MISSING)
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.

separator = ','
escape_char = '\\'
def convert(self, value: str) -> list[~T]:
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.

class NoneType(confkit.BaseDataType[NoneType]):
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.

NoneType()
125    def __init__(self) -> None:
126        """Initialize the NoneType data type."""
127        super().__init__(None)

Initialize the NoneType data type.

null_values: ClassVar[set[str]] = {'nil', 'null', 'none'}
def convert(self, value: str) -> bool:
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.

class Octal(confkit.BaseDataType[int]):
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.

Octal(default: int, base: int = 8)
232    def __init__(self, default: int, base: int = OCTAL) -> None:  # noqa: D107
233        super().__init__(default, base)

Initialize the base data type.

def convert(self, value: str) -> int:
238    def convert(self, value: str) -> int:
239        """Convert a string value to an integer from octal."""
240        return int(value.removeprefix("0o"), 8)

Convert a string value to an integer from octal.

class Optional(confkit.BaseDataType[typing.Optional[~T]], typing.Generic[~T]):
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.

Optional(data_type: BaseDataType[~T])
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.

default: Optional[~T]
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.

value: Optional[~T]
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.

def convert(self, value: str) -> Optional[~T]:
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)

Convert a string value to the optional type.

def validate(self) -> bool:
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()

Validate that the value is of the wrapped data type or None.

class StrEnum(confkit.BaseDataType[enum.StrEnum]):
 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.

def convert(self, value: str) -> enum.StrEnum:
102    def convert(self, value: str) -> enum.StrEnum:
103        """Convert a string value to an enum."""
104        return self.value.__class__(value)

Convert a string value to an enum.

class String(confkit.BaseDataType[str]):
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.

String(default: str = '')
139    def __init__(self, default: str = "") -> None:  # noqa: D107
140        super().__init__(default)

Initialize the base data type.

def convert(self, value: str) -> str:
142    def convert(self, value: str) -> str:
143        """Convert a string value to a string."""
144        return value

Convert a string value to a string.