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]
class BaseDataType(abc.ABC, typing.Generic[~T]):
23class BaseDataType(ABC, Generic[T]):
24    """Base class used for Config descriptors to define a data type."""
25
26    def __init__(self, default: T) -> None:
27        """Initialize the base data type."""
28        self.default = default
29        self.value = default
30        self.type = type(default)
31
32    def __str__(self) -> str:
33        """Return the string representation of the stored value."""
34        return str(self.value)
35
36    @abstractmethod
37    def convert(self, value: str) -> T:
38        """Convert a string value to the desired type."""
39
40    def validate(self) -> bool:
41        """Validate that the value matches the expected type."""
42        orig_bases: tuple[type, ...] | None = getattr(self.__class__, "__orig_bases__", None)
43
44        if not orig_bases:
45            msg = "No type information available for validation."
46            raise InvalidConverterError(msg)
47
48        # Extract type arguments from the generic base
49        for base in orig_bases:
50            if hasattr(base, "__args__"):
51                type_args = base.__args__
52                if type_args:
53                    for type_arg in type_args:
54                        if hasattr(type_arg, "__origin__"):
55                            # For parameterized generics, check against the origin type
56                            if isinstance(self.value, type_arg.__origin__):
57                                return True
58                        elif isinstance(self.value, (self.type, type_arg)):
59                            return True
60                    msg = f"Value {self.value} is not any of {type_args}."
61                    raise InvalidConverterError(msg)
62        msg = "This should not have raised. Report to the library maintainers with code: `DTBDT`"
63        raise TypeError(msg)
64
65    @staticmethod
66    def cast_optional(default: T | None | BaseDataType[T]) -> BaseDataType[T | None]:
67        """Convert the default value to an Optional data type."""
68        if default is None:
69            return cast("BaseDataType[T | None]", NoneType())
70        return Optional(BaseDataType.cast(default))
71
72    @staticmethod
73    def cast(default: T | BaseDataType[T]) -> BaseDataType[T]:  # noqa: C901, PLR0911
74        """Convert the default value to a BaseDataType."""
75        # We use Cast to shut up type checkers, as we know primitive types will be correct.
76        # If a custom type is passed, it should be a BaseDataType subclass, which already has the correct types.
77        # Check enum types BEFORE basic types since some enums inherit from str/int
78        match default:
79            case dStrEnum():     return cast("BaseDataType[T]", StrEnum(default))
80            case dIntFlag():     return cast("BaseDataType[T]", IntFlag(default))
81            case dIntEnum():     return cast("BaseDataType[T]", IntEnum(default))
82            case dEnum():        return cast("BaseDataType[T]", Enum(default))
83            case bool():         return cast("BaseDataType[T]", Boolean(default))
84            case None:           return cast("BaseDataType[T]", NoneType())
85            case int():          return cast("BaseDataType[T]", Integer(default))
86            case float():        return cast("BaseDataType[T]", Float(default))
87            case str():          return cast("BaseDataType[T]", String(default))
88            case BaseDataType(): return default
89            case _:
90                msg = (
91                    f"Unsupported default value type: {type(default).__name__}. "
92                    "Use a BaseDataType subclass for custom types."
93                )
94                raise InvalidDefaultError(msg)

Base class used for Config descriptors to define a data type.

BaseDataType(default: ~T)
26    def __init__(self, default: T) -> None:
27        """Initialize the base data type."""
28        self.default = default
29        self.value = default
30        self.type = type(default)

Initialize the base data type.

default
value
type
@abstractmethod
def convert(self, value: str) -> ~T:
36    @abstractmethod
37    def convert(self, value: str) -> T:
38        """Convert a string value to the desired type."""

Convert a string value to the desired type.

def validate(self) -> bool:
40    def validate(self) -> bool:
41        """Validate that the value matches the expected type."""
42        orig_bases: tuple[type, ...] | None = getattr(self.__class__, "__orig_bases__", None)
43
44        if not orig_bases:
45            msg = "No type information available for validation."
46            raise InvalidConverterError(msg)
47
48        # Extract type arguments from the generic base
49        for base in orig_bases:
50            if hasattr(base, "__args__"):
51                type_args = base.__args__
52                if type_args:
53                    for type_arg in type_args:
54                        if hasattr(type_arg, "__origin__"):
55                            # For parameterized generics, check against the origin type
56                            if isinstance(self.value, type_arg.__origin__):
57                                return True
58                        elif isinstance(self.value, (self.type, type_arg)):
59                            return True
60                    msg = f"Value {self.value} is not any of {type_args}."
61                    raise InvalidConverterError(msg)
62        msg = "This should not have raised. Report to the library maintainers with code: `DTBDT`"
63        raise TypeError(msg)

Validate that the value matches the expected type.

@staticmethod
def cast_optional( default: Union[~T, NoneType, BaseDataType[~T]]) -> BaseDataType[typing.Optional[~T]]:
65    @staticmethod
66    def cast_optional(default: T | None | BaseDataType[T]) -> BaseDataType[T | None]:
67        """Convert the default value to an Optional data type."""
68        if default is None:
69            return cast("BaseDataType[T | None]", NoneType())
70        return Optional(BaseDataType.cast(default))

Convert the default value to an Optional data type.

@staticmethod
def cast( default: Union[~T, BaseDataType[~T]]) -> BaseDataType[~T]:
72    @staticmethod
73    def cast(default: T | BaseDataType[T]) -> BaseDataType[T]:  # noqa: C901, PLR0911
74        """Convert the default value to a BaseDataType."""
75        # We use Cast to shut up type checkers, as we know primitive types will be correct.
76        # If a custom type is passed, it should be a BaseDataType subclass, which already has the correct types.
77        # Check enum types BEFORE basic types since some enums inherit from str/int
78        match default:
79            case dStrEnum():     return cast("BaseDataType[T]", StrEnum(default))
80            case dIntFlag():     return cast("BaseDataType[T]", IntFlag(default))
81            case dIntEnum():     return cast("BaseDataType[T]", IntEnum(default))
82            case dEnum():        return cast("BaseDataType[T]", Enum(default))
83            case bool():         return cast("BaseDataType[T]", Boolean(default))
84            case None:           return cast("BaseDataType[T]", NoneType())
85            case int():          return cast("BaseDataType[T]", Integer(default))
86            case float():        return cast("BaseDataType[T]", Float(default))
87            case str():          return cast("BaseDataType[T]", String(default))
88            case BaseDataType(): return default
89            case _:
90                msg = (
91                    f"Unsupported default value type: {type(default).__name__}. "
92                    "Use a BaseDataType subclass for custom types."
93                )
94                raise InvalidDefaultError(msg)

Convert the default value to a BaseDataType.

class Binary(confkit.BaseDataType[bytes | int]):
327class Binary(BaseDataType[bytes | int]):
328    """A config value that represents binary."""
329
330    def __init__(self, default: bytes | int = 0) -> None:  # noqa: D107
331        if isinstance(default, bytes):
332            default = int.from_bytes(default)
333        super().__init__(default)
334
335    def __str__(self) -> str:  # noqa: D105
336        if isinstance(self.value, bytes):
337            self.value = int.from_bytes(self.value)
338        return f"0b{self.value:b}"
339
340    def convert(self, value: str) -> int:
341        """Convert a string value to an integer from binary."""
342        return int(value.removeprefix("0b"), 2)

A config value that represents binary.

Binary(default: bytes | int = 0)
330    def __init__(self, default: bytes | int = 0) -> None:  # noqa: D107
331        if isinstance(default, bytes):
332            default = int.from_bytes(default)
333        super().__init__(default)

Initialize the base data type.

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

Convert a string value to an integer from binary.

class Boolean(confkit.BaseDataType[bool]):
243class Boolean(BaseDataType[bool]):
244    """A config value that is a boolean."""
245
246    def __init__(self, default: bool = False) -> None:  # noqa: D107, FBT001, FBT002
247        super().__init__(default)
248
249    def convert(self, value: str) -> bool:
250        """Convert a string value to a boolean."""
251        if value.lower() in {"true", "1", "yes"}:
252            return True
253        if value.lower() in {"false", "0", "no"}:
254            return False
255        msg = f"Cannot convert {value} to boolean."
256        raise ValueError(msg)

A config value that is a boolean.

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

Initialize the base data type.

def convert(self, value: str) -> bool:
249    def convert(self, value: str) -> bool:
250        """Convert a string value to a boolean."""
251        if value.lower() in {"true", "1", "yes"}:
252            return True
253        if value.lower() in {"false", "0", "no"}:
254            return False
255        msg = f"Cannot convert {value} to boolean."
256        raise ValueError(msg)

Convert a string value to a boolean.

class Config(typing.Generic[~VT]):
 48class Config(Generic[VT]):
 49    """A descriptor for config values, preserving type information.
 50
 51    the ValueType (VT) is the type you want the config value to be.
 52    """
 53
 54    validate_types: ClassVar[bool] = True
 55    """Validate that the converter returns the same type as the default value. (not strict)"""
 56    write_on_edit: ClassVar[bool] = True
 57    """Write to the config file when updating a value."""
 58    optional: bool = False
 59    """If True, allows None as an extra type when validating types. (both instance and class variables.)"""
 60
 61    _parser: ConfkitParser = UNSET
 62    _file: Path = UNSET
 63    _has_read_config: bool = False
 64    _data_type: BaseDataType[VT]
 65
 66    if TYPE_CHECKING:
 67        # Overloads for type checkers to understand the different settings of the Config descriptors.
 68        @overload # Custom data type, like Enum's or custom class.
 69        def __init__(self, default: BaseDataType[VT]) -> None: ...
 70        @overload
 71        def __init__(self, default: VT) -> None: ...
 72        # Specify the states of optional explicitly for type checkers.
 73        @overload
 74        def __init__(self: Config[OVT], default: OVT, *, optional: Literal[False]) -> None: ...
 75        @overload
 76        def __init__(self: Config[OVT], default: BaseDataType[OVT], *, optional: Literal[False]) -> None: ...
 77        @overload
 78        def __init__(self: Config[OVT | None], default: OVT, *, optional: Literal[True]) -> None: ...
 79        @overload
 80        def __init__(self: Config[OVT | None], default: BaseDataType[OVT], *, optional: Literal[True]) -> None: ...
 81
 82    def __init__(
 83        self,
 84        default: VT | None | BaseDataType[VT] = UNSET,
 85        *,
 86        optional: bool = False,
 87    ) -> None:
 88        """Initialize the config descriptor with a default value.
 89
 90        Validate that parser and filepath are present.
 91        """
 92        cls = self.__class__
 93        self.optional = optional or cls.optional # Be truthy when either one is true.
 94
 95        if not self.optional and default is UNSET:
 96            msg = "Default value cannot be None when optional is False."
 97            raise InvalidDefaultError(msg)
 98
 99        if not self._parser:
100            self._detect_parser()
101
102        self._initialize_data_type(default)
103        self._validate_init()
104        self._read_parser()
105
106    def __init_subclass__(cls) -> None:
107        """Allow for multiple config files/parsers without conflicts."""
108        super().__init_subclass__()
109
110        parent = cls._find_parent()
111
112        cls.validate_types = parent.validate_types
113        cls.write_on_edit = parent.write_on_edit
114        cls._parser = parent._parser  # noqa: SLF001
115        cls._file = parent._file  # noqa: SLF001
116        cls._has_read_config = parent._has_read_config  # noqa: SLF001
117
118    @classmethod
119    def _find_parent(cls) -> type[Config[Any]]:
120        for base in cls.__bases__:
121            if issubclass(base, Config):
122                parent = base
123                break
124        else:
125            parent = Config
126        return parent
127
128    def _initialize_data_type(self, default: VT | None | BaseDataType[VT]) -> None:
129        """Initialize the data type based on the default value."""
130        if not self.optional and default is not None:
131            self._data_type = BaseDataType[VT].cast(default)
132        else:
133            self._data_type = BaseDataType[VT].cast_optional(default)
134
135    def _read_parser(self) -> None:
136        """Ensure the parser has read the file at initialization. Avoids rewriting the file when settings are already set."""
137        cls = self.__class__
138        if not cls._has_read_config:
139            self._parser.read(self._file)
140            cls._has_read_config = True
141
142    def _validate_init(self) -> None:
143        """Validate the config descriptor, ensuring it's properly set up."""
144        self.validate_file()
145        self.validate_parser()
146
147    def convert(self, value: str) -> VT:
148        """Convert the value to the desired type using the given converter method."""
149        return self._data_type.convert(value)
150
151    @staticmethod
152    def _warn_base_class_usage() -> None:
153        """Warn users that setting parser/file on the base class can lead to unexpected behavior.
154        Tell the user to subclass <Config> first.
155        """  # noqa: D205
156        warnings.warn("<Config> is the base class. Subclass <Config> to avoid unexpected behavior.", stacklevel=2)
157
158    @classmethod
159    @deprecated("Avoid using set_parser. Confkit will automatically assign a parser based on the file extension. In 2.0 this will be a private method.")  # noqa: E501
160    def set_parser(cls, parser: ConfkitParser) -> None:
161        """Set the parser for ALL descriptor instances (of this type/class)."""
162        if cls is Config:
163            cls._warn_base_class_usage()
164        cls._parser = parser
165
166    @classmethod
167    def _detect_parser(cls) -> None:
168        """Set the parser for descriptors based on the file extension of cls._file.
169
170        Uses msgspec-based parsers for yaml, json, toml. Defaults to dict structure.
171        Only sets the parser if there is no parser set.
172        """
173        if cls._file is UNSET:
174            msg = "Config file is not set. Use `set_file()`."
175            raise ValueError(msg)
176        match cls._file.suffix.lower():
177            case ".ini":
178                cls._parser = IniParser()
179            case ".yaml" | ".yml" | ".json" | ".toml":
180                from confkit.ext.parsers import MsgspecParser  # noqa: PLC0415  Only import if actually used.
181                cls._parser = MsgspecParser()
182            case ".env":
183                cls._parser = EnvParser()
184            case _:
185                msg = f"Unsupported config file extension: {cls._file.suffix.lower()}"
186                raise ValueError(msg)
187
188    @classmethod
189    def set_file(cls, file: Path) -> None:
190        """Set the file for ALL descriptors."""
191        if cls is Config:
192            cls._warn_base_class_usage()
193        cls._file = file
194        cls._watcher = FileWatcher(file)
195
196    def validate_strict_type(self) -> None:
197        """Validate the type of the converter matches the desired type."""
198        if self._data_type.convert is UNSET:
199            msg = "Converter is not set."
200            raise InvalidConverterError(msg)
201
202        cls = self.__class__
203        self.__config_value = cls._parser.get(self._section, self._setting)
204        self.__converted_value = self.convert(self.__config_value)
205
206        if not cls.validate_types:
207            return
208
209        self.__converted_type = type(self.__converted_value)
210        default_value_type = type(self._data_type.default)
211
212        is_optional = self.optional or isinstance(self._data_type, Optional)
213        if (is_optional) and self.__converted_type in (default_value_type, NoneType):
214            # Allow None or the same type as the default value to be returned by the converter when _optional is True.
215            return
216        if self.__converted_type is not default_value_type:
217            msg = f"Converter does not return the same type as the default value <{default_value_type}> got <{self.__converted_type}>."  # noqa: E501
218            raise InvalidConverterError(msg)
219
220        # Set the data_type value. ensuring validation works as expected.
221        self._data_type.value = self.__converted_value
222        if not self._data_type.validate():
223            msg = f"Invalid value for {self._section}.{self._setting}: {self.__converted_value}"
224            raise InvalidConverterError(msg)
225
226    @classmethod
227    def validate_file(cls) -> None:
228        """Validate the config file."""
229        if cls._file is UNSET:
230            msg = f"Config file is not set. use {cls.__name__}.set_file() to set it."
231            raise ValueError(msg)
232
233    @classmethod
234    def validate_parser(cls) -> None:
235        """Validate the config parser."""
236        if cls._parser is UNSET:
237            msg = f"Config parser is not set. use {cls.__name__}.set_parser() to set it."
238            raise ValueError(msg)
239
240    def __set_name__(self, owner: type, name: str) -> None:
241        """Set the name of the attribute to the name of the descriptor."""
242        self.name = name
243        self._section = self._build_section_name(owner)
244        self._setting = name
245        self._ensure_option()
246        cls = self.__class__
247        self._original_value = cls._parser.get(self._section, self._setting) or self._data_type.default
248        self.private = f"_{self._section}_{self._setting}_{self.name}"
249
250    @staticmethod
251    def _build_section_name(owner: type) -> str:
252        """Build a section name from the class hierarchy using dot notation.
253
254        Strips out function-local scope markers like <locals>.
255        """
256        if qualname := getattr(owner, "__qualname__", None):
257            split_at = qualname.find("<locals>.")
258            if split_at != -1:
259                qualname = qualname[split_at + len("<locals>.") :]
260            return ".".join(
261                part
262                for part in qualname.split(".")
263            )
264        return owner.__name__
265
266    def _ensure_section(self) -> None:
267        """Ensure the section exists in the config file. Creates one if it doesn't exist."""
268        if not self._parser.has_section(self._section):
269            self._parser.add_section(self._section)
270
271    def _ensure_option(self) -> None:
272        """Ensure the option exists in the config file. Creates one if it doesn't exist."""
273        self._ensure_section()
274        if not self._parser.has_option(self._section, self._setting):
275            cls = self.__class__
276            cls._set(self._section, self._setting, self._data_type)
277
278    def __get__(self, obj: object, obj_type: object) -> VT:
279        """Get the value of the attribute."""
280        # obj_type is the class in which the variable is defined
281        # so it can be different than type of VT
282        # but we don't need obj or it's type to get the value from config in our case.
283        if self._watcher.has_changed():
284            self.on_file_change(
285                "get",
286                self._data_type.value,
287                self.convert(self._parser.get(self._section, self._setting)),
288            )
289
290        self.validate_strict_type()
291        return self.__converted_value  # This is already used when checking type validation, so it's safe to return it.
292
293    def __set__(self, obj: object, value: VT) -> None:
294        """Set the value of the attribute."""
295        if self._watcher.has_changed():
296            self.on_file_change("set", self._data_type.value, value)
297
298        self._data_type.value = value
299        cls = self.__class__
300        cls._set(self._section, self._setting, self._data_type)
301        setattr(obj, self.private, value)
302
303    @classmethod
304    def _set(cls, section: str, setting: str, value: VT | BaseDataType[VT] | BaseDataType[VT | None]) -> None:
305        """Set a config value, and write it to the file."""
306        if not cls._parser.has_section(section):
307            cls._parser.add_section(section)
308
309        cls._parser.set(section, setting, value)
310
311        if cls.write_on_edit:
312            cls.write()
313
314
315    @classmethod
316    def write(cls) -> None:
317        """Write the config parser to the file."""
318        cls.validate_file()
319        with cls._file.open("w") as f:
320            cls._parser.write(f)
321
322    @classmethod
323    def set(cls, section: str, setting: str, value: VT) -> Callable[[Callable[P, F]], Callable[P, F]]:
324        """Set a config value using this descriptor."""
325
326        def wrapper(func: Callable[P, F]) -> Callable[P, F]:
327            @wraps(func)
328            def inner(*args: P.args, **kwargs: P.kwargs) -> F:
329                cls._set(section, setting, value)
330                return func(*args, **kwargs)
331
332            return inner
333        return wrapper
334
335
336    @classmethod
337    def with_setting(cls, setting: Config[OVT]) -> Callable[[Callable[P, F]], Callable[P, F]]:
338        """Insert a config value into **kwargs to the wrapped method/function using this decorator."""
339        def wrapper(func: Callable[P, F]) -> Callable[P, F]:
340            @wraps(func)
341            def inner(*args: P.args, **kwargs: P.kwargs) -> F:
342                kwargs[setting.name] = setting.convert(cls._parser.get(setting._section, setting._setting))
343                return func(*args, **kwargs)
344
345            return inner
346        return wrapper
347
348
349    @classmethod
350    def with_kwarg(
351        cls, section: str, setting: str, name: str | None = None, default: VT = UNSET,
352    ) -> Callable[[Callable[P, F]], Callable[P, F]]:
353        """Insert a config value into **kwargs to the wrapped method/function using this descriptor.
354
355        Use kwarg.get(`name`) to get the value.
356        `name` is the name the kwarg gets if passed, if None, it will be the same as `setting`.
357        Section parameter is just for finding the config value.
358        """
359        if name is None:
360            name = setting
361        if default is UNSET and not cls._parser.has_option(section, setting):
362            msg = f"Config value {section=} {setting=} is not set. and no default value is given."
363            raise ValueError(msg)
364
365        def wrapper(func: Callable[P, F]) -> Callable[P, F]:
366            @wraps(func)
367            def inner(*args: P.args, **kwargs: P.kwargs) -> F:
368                if default is not UNSET:
369                    cls._set_default(section, setting, default)
370                kwargs[name] = cls._parser.get(section, setting)
371                return func(*args, **kwargs)
372
373            return inner
374        return wrapper
375
376    @classmethod
377    def _set_default(cls, section: str, setting: str, value: VT) -> None:
378        if cls._parser.get(section, setting, fallback=UNSET) is UNSET:
379            cls._set(section, setting, value)
380
381    @classmethod
382    def default(cls, section: str, setting: str, value: VT) -> Callable[[Callable[P, F]], Callable[P, F]]:
383        """Set a default config value if none are set yet using this descriptor."""
384        def wrapper(func: Callable[P, F]) -> Callable[P, F]:
385            @wraps(func)
386            def inner(*args: P.args, **kwargs: P.kwargs) -> F:
387                cls._set_default(section, setting, value)
388                return func(*args, **kwargs)
389
390            return inner
391        return wrapper
392
393    @abstractmethod
394    def on_file_change(self, origin: Literal["get", "set"], old: VT | UNSET, new: VT) -> None:
395        """Triggered when the config file changes.
396
397        This needs to be implemented before it's usable.
398        This will be called **before** setting the value from the config file.
399        This will be called **after** getting (but before validating it's type) the value from config file.
400        The `origin` parameter indicates whether the change was triggered by a `get` or `set` operation.
401        """

A descriptor for config values, preserving type information.

the ValueType (VT) is the type you want the config value to be.

Config( default: Union[~VT, NoneType, BaseDataType[~VT]] = MISSING, *, optional: bool = False)
 82    def __init__(
 83        self,
 84        default: VT | None | BaseDataType[VT] = UNSET,
 85        *,
 86        optional: bool = False,
 87    ) -> None:
 88        """Initialize the config descriptor with a default value.
 89
 90        Validate that parser and filepath are present.
 91        """
 92        cls = self.__class__
 93        self.optional = optional or cls.optional # Be truthy when either one is true.
 94
 95        if not self.optional and default is UNSET:
 96            msg = "Default value cannot be None when optional is False."
 97            raise InvalidDefaultError(msg)
 98
 99        if not self._parser:
100            self._detect_parser()
101
102        self._initialize_data_type(default)
103        self._validate_init()
104        self._read_parser()

Initialize the config descriptor with a default value.

Validate that parser and filepath are present.

validate_types: ClassVar[bool] = True

Validate that the converter returns the same type as the default value. (not strict)

write_on_edit: ClassVar[bool] = True

Write to the config file when updating a value.

optional: bool = False

If True, allows None as an extra type when validating types. (both instance and class variables.)

def convert(self, value: str) -> ~VT:
147    def convert(self, value: str) -> VT:
148        """Convert the value to the desired type using the given converter method."""
149        return self._data_type.convert(value)

Convert the value to the desired type using the given converter method.

@classmethod
@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.')
def set_parser(cls, parser: confkit.parsers.ConfkitParser) -> None:
158    @classmethod
159    @deprecated("Avoid using set_parser. Confkit will automatically assign a parser based on the file extension. In 2.0 this will be a private method.")  # noqa: E501
160    def set_parser(cls, parser: ConfkitParser) -> None:
161        """Set the parser for ALL descriptor instances (of this type/class)."""
162        if cls is Config:
163            cls._warn_base_class_usage()
164        cls._parser = parser

Set the parser for ALL descriptor instances (of this type/class).

@classmethod
def set_file(cls, file: pathlib.Path) -> None:
188    @classmethod
189    def set_file(cls, file: Path) -> None:
190        """Set the file for ALL descriptors."""
191        if cls is Config:
192            cls._warn_base_class_usage()
193        cls._file = file
194        cls._watcher = FileWatcher(file)

Set the file for ALL descriptors.

def validate_strict_type(self) -> None:
196    def validate_strict_type(self) -> None:
197        """Validate the type of the converter matches the desired type."""
198        if self._data_type.convert is UNSET:
199            msg = "Converter is not set."
200            raise InvalidConverterError(msg)
201
202        cls = self.__class__
203        self.__config_value = cls._parser.get(self._section, self._setting)
204        self.__converted_value = self.convert(self.__config_value)
205
206        if not cls.validate_types:
207            return
208
209        self.__converted_type = type(self.__converted_value)
210        default_value_type = type(self._data_type.default)
211
212        is_optional = self.optional or isinstance(self._data_type, Optional)
213        if (is_optional) and self.__converted_type in (default_value_type, NoneType):
214            # Allow None or the same type as the default value to be returned by the converter when _optional is True.
215            return
216        if self.__converted_type is not default_value_type:
217            msg = f"Converter does not return the same type as the default value <{default_value_type}> got <{self.__converted_type}>."  # noqa: E501
218            raise InvalidConverterError(msg)
219
220        # Set the data_type value. ensuring validation works as expected.
221        self._data_type.value = self.__converted_value
222        if not self._data_type.validate():
223            msg = f"Invalid value for {self._section}.{self._setting}: {self.__converted_value}"
224            raise InvalidConverterError(msg)

Validate the type of the converter matches the desired type.

@classmethod
def validate_file(cls) -> None:
226    @classmethod
227    def validate_file(cls) -> None:
228        """Validate the config file."""
229        if cls._file is UNSET:
230            msg = f"Config file is not set. use {cls.__name__}.set_file() to set it."
231            raise ValueError(msg)

Validate the config file.

@classmethod
def validate_parser(cls) -> None:
233    @classmethod
234    def validate_parser(cls) -> None:
235        """Validate the config parser."""
236        if cls._parser is UNSET:
237            msg = f"Config parser is not set. use {cls.__name__}.set_parser() to set it."
238            raise ValueError(msg)

Validate the config parser.

@classmethod
def write(cls) -> None:
315    @classmethod
316    def write(cls) -> None:
317        """Write the config parser to the file."""
318        cls.validate_file()
319        with cls._file.open("w") as f:
320            cls._parser.write(f)

Write the config parser to the file.

@classmethod
def set( cls, section: str, setting: str, value: ~VT) -> Callable[[Callable[~P, ~F]], Callable[~P, ~F]]:
322    @classmethod
323    def set(cls, section: str, setting: str, value: VT) -> Callable[[Callable[P, F]], Callable[P, F]]:
324        """Set a config value using this descriptor."""
325
326        def wrapper(func: Callable[P, F]) -> Callable[P, F]:
327            @wraps(func)
328            def inner(*args: P.args, **kwargs: P.kwargs) -> F:
329                cls._set(section, setting, value)
330                return func(*args, **kwargs)
331
332            return inner
333        return wrapper

Set a config value using this descriptor.

@classmethod
def with_setting( cls, setting: Config[~OVT]) -> Callable[[Callable[~P, ~F]], Callable[~P, ~F]]:
336    @classmethod
337    def with_setting(cls, setting: Config[OVT]) -> Callable[[Callable[P, F]], Callable[P, F]]:
338        """Insert a config value into **kwargs to the wrapped method/function using this decorator."""
339        def wrapper(func: Callable[P, F]) -> Callable[P, F]:
340            @wraps(func)
341            def inner(*args: P.args, **kwargs: P.kwargs) -> F:
342                kwargs[setting.name] = setting.convert(cls._parser.get(setting._section, setting._setting))
343                return func(*args, **kwargs)
344
345            return inner
346        return wrapper

Insert a config value into **kwargs to the wrapped method/function using this decorator.

@classmethod
def with_kwarg( cls, section: str, setting: str, name: str | None = None, default: ~VT = MISSING) -> Callable[[Callable[~P, ~F]], Callable[~P, ~F]]:
349    @classmethod
350    def with_kwarg(
351        cls, section: str, setting: str, name: str | None = None, default: VT = UNSET,
352    ) -> Callable[[Callable[P, F]], Callable[P, F]]:
353        """Insert a config value into **kwargs to the wrapped method/function using this descriptor.
354
355        Use kwarg.get(`name`) to get the value.
356        `name` is the name the kwarg gets if passed, if None, it will be the same as `setting`.
357        Section parameter is just for finding the config value.
358        """
359        if name is None:
360            name = setting
361        if default is UNSET and not cls._parser.has_option(section, setting):
362            msg = f"Config value {section=} {setting=} is not set. and no default value is given."
363            raise ValueError(msg)
364
365        def wrapper(func: Callable[P, F]) -> Callable[P, F]:
366            @wraps(func)
367            def inner(*args: P.args, **kwargs: P.kwargs) -> F:
368                if default is not UNSET:
369                    cls._set_default(section, setting, default)
370                kwargs[name] = cls._parser.get(section, setting)
371                return func(*args, **kwargs)
372
373            return inner
374        return wrapper

Insert a config value into **kwargs to the wrapped method/function using this descriptor.

Use kwarg.get(name) to get the value. name is the name the kwarg gets if passed, if None, it will be the same as setting. Section parameter is just for finding the config value.

@classmethod
def default( cls, section: str, setting: str, value: ~VT) -> Callable[[Callable[~P, ~F]], Callable[~P, ~F]]:
381    @classmethod
382    def default(cls, section: str, setting: str, value: VT) -> Callable[[Callable[P, F]], Callable[P, F]]:
383        """Set a default config value if none are set yet using this descriptor."""
384        def wrapper(func: Callable[P, F]) -> Callable[P, F]:
385            @wraps(func)
386            def inner(*args: P.args, **kwargs: P.kwargs) -> F:
387                cls._set_default(section, setting, value)
388                return func(*args, **kwargs)
389
390            return inner
391        return wrapper

Set a default config value if none are set yet using this descriptor.

@abstractmethod
def on_file_change( self, origin: Literal['get', 'set'], old: Union[~VT, MISSING], new: ~VT) -> None:
393    @abstractmethod
394    def on_file_change(self, origin: Literal["get", "set"], old: VT | UNSET, new: VT) -> None:
395        """Triggered when the config file changes.
396
397        This needs to be implemented before it's usable.
398        This will be called **before** setting the value from the config file.
399        This will be called **after** getting (but before validating it's type) the value from config file.
400        The `origin` parameter indicates whether the change was triggered by a `get` or `set` operation.
401        """

Triggered when the config file changes.

This needs to be implemented before it's usable. This will be called before setting the value from the config file. This will be called after getting (but before validating it's type) the value from config file. The origin parameter indicates whether the change was triggered by a get or set operation.

class ConfigContainerMeta(builtins.type):
37class ConfigContainerMeta(type):
38    """Metaclass for Config to "force" __set__ to be called on class variables."""
39
40    def __setattr__(cls, key: str, value: object) -> None:
41        """Set the value of the attribute on the class."""
42        attr = cls.__dict__.get(key)
43        if isinstance(attr, Config):
44            attr.__set__(cls, value)
45        else:
46            super().__setattr__(key, value)

Metaclass for Config to "force" __set__ to be called on class variables.

class ConfigPathConflictError(builtins.ValueError):
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.

class Date(confkit.BaseDataType[datetime.date]):
655class Date(BaseDataType[date]):
656    """A config value that is a date."""
657
658    @overload
659    def __init__(self, default: date = UNSET) -> None: ...
660    @overload
661    def __init__(self, **kwargs: Unpack[_DateKwargs]) -> None: ...
662
663    def __init__(self, default: date = UNSET, **kwargs: Unpack[_DateKwargs]) -> None:
664        """Initialize the date data type. Defaults to current date if not provided."""
665        if default is UNSET:
666            default = date(**kwargs)
667        super().__init__(default)
668
669    def convert(self, value: str) -> date:
670        """Convert a string value to a date."""
671        return date.fromisoformat(value)
672
673    def __str__(self) -> str:  # noqa: D105
674        return self.value.isoformat()

A config value that is a date.

Date( default: datetime.date = MISSING, **kwargs: *<class 'confkit.data_types._DateKwargs'>)
663    def __init__(self, default: date = UNSET, **kwargs: Unpack[_DateKwargs]) -> None:
664        """Initialize the date data type. Defaults to current date if not provided."""
665        if default is UNSET:
666            default = date(**kwargs)
667        super().__init__(default)

Initialize the date data type. Defaults to current date if not provided.

def convert(self, value: str) -> datetime.date:
669    def convert(self, value: str) -> date:
670        """Convert a string value to a date."""
671        return date.fromisoformat(value)

Convert a string value to a date.

class DateTime(confkit.BaseDataType[datetime.datetime]):
625class DateTime(BaseDataType[datetime]):
626    """A config value that is a datetime."""
627
628    @overload
629    def __init__(self, default: datetime = UNSET) -> None: ...
630    @overload
631    def __init__(self, **kwargs: Unpack[_DateTimeKwargs]) -> None: ...
632
633    def __init__(self, default: datetime = UNSET, **kwargs: Unpack[_DateTimeKwargs]) -> None:
634        """Initialize the datetime data type. Defaults to current datetime (datetime.now) if not provided."""
635        if default is UNSET:
636            try:
637                default = datetime(**kwargs)  # noqa: DTZ001 Tzinfo is (optionally) passed using kwargs
638            except TypeError:
639                default = datetime.now(tz=UTC)
640        super().__init__(default)
641
642    def convert(self, value: str) -> datetime:
643        """Convert a string value to a datetime."""
644        return datetime.fromisoformat(value)
645
646    def __str__(self) -> str:
647        """Return the string representation of the stored value."""
648        return self.value.isoformat()

A config value that is a datetime.

DateTime( default: datetime.datetime = MISSING, **kwargs: *<class 'confkit.data_types._DateTimeKwargs'>)
633    def __init__(self, default: datetime = UNSET, **kwargs: Unpack[_DateTimeKwargs]) -> None:
634        """Initialize the datetime data type. Defaults to current datetime (datetime.now) if not provided."""
635        if default is UNSET:
636            try:
637                default = datetime(**kwargs)  # noqa: DTZ001 Tzinfo is (optionally) passed using kwargs
638            except TypeError:
639                default = datetime.now(tz=UTC)
640        super().__init__(default)

Initialize the datetime data type. Defaults to current datetime (datetime.now) if not provided.

def convert(self, value: str) -> datetime.datetime:
642    def convert(self, value: str) -> datetime:
643        """Convert a string value to a datetime."""
644        return datetime.fromisoformat(value)

Convert a string value to a datetime.

class Dict(confkit.BaseDataType[dict[~KT, ~VT]], typing.Generic[~KT, ~VT]):
531class Dict(BaseDataType[dict[KT, VT]], Generic[KT, VT]):
532    """A config value that is a dictionary of string keys and values of type T."""
533
534    @overload
535    def __init__(self, default: dict[KT, VT]) -> None: ...
536    @overload
537    def __init__(self, *, key_type: BaseDataType[KT], value_type: BaseDataType[VT]) -> None: ...
538    @overload
539    def __init__(
540        self,
541        default: dict[KT, VT],
542        *,
543        key_type: BaseDataType[KT] = ...,
544        value_type: BaseDataType[VT] = ...,
545    ) -> None: ...
546
547    def __init__(
548        self,
549        default: dict[KT, VT] = UNSET,
550        *,
551        key_type: BaseDataType[KT] = UNSET,
552        value_type: BaseDataType[VT] = UNSET,
553    ) -> None:
554        """Initialize the dict data type."""
555        if default is UNSET and (key_type is UNSET or value_type is UNSET):
556            msg = "Dict requires either a default with at least one key/value pair, or both key_type and value_type to be specified."  # noqa: E501
557            raise InvalidDefaultError(msg)
558        if default is UNSET:
559            default = {}
560        super().__init__(default)
561
562        self._infer_key_type(default, key_type)
563        self._infer_value_type(default, value_type)
564
565    def _infer_key_type(self, default: dict[KT, VT], key_type: BaseDataType[KT]) -> None:
566        """Infer the key type from the default dictionary if not provided."""
567        if len(default.keys()) <= 0 and key_type is UNSET:
568            msg = "Dict default must have at least one key element to infer type. or specify `key_type=<BaseDataType>`"
569            raise InvalidDefaultError(msg)
570        if key_type is UNSET:
571            for key in default:
572                self._key_data_type = BaseDataType[KT].cast(key)
573                break
574        else:
575            self._key_data_type = key_type
576
577    def _infer_value_type(self, default: dict[KT, VT], value_type: BaseDataType[VT]) -> None:
578        """Infer the value type from the default dictionary if not provided."""
579        if len(default.values()) <= 0 and value_type is UNSET:
580            msg = "Dict default must have at least one value element to infer type. or specify `value_type=<BaseDataType>`"
581            raise InvalidDefaultError(msg)
582        if value_type is UNSET:
583            for value in default.values():
584                self._value_data_type = BaseDataType[VT].cast(value)
585                break
586        else:
587            self._value_data_type = value_type
588
589    def convert(self, value: str) -> dict[KT, VT]:
590        """Convert a string to a dictionary."""
591        if not value:
592            return {}
593
594        parts = value.split(",")
595        result: dict[KT, VT] = {}
596        for part in parts:
597            if "=" not in part:
598                msg = f"Invalid dictionary entry: {part}. Expected format key=value."
599                raise ValueError(msg)
600            key_str, val_str = part.split("=", 1)
601            key = self._key_data_type.convert(key_str.strip())
602            val = self._value_data_type.convert(val_str.strip())
603            result[key] = val
604        return result
605
606    def __str__(self) -> str:
607        """Return a string representation of the dictionary."""
608        items = [
609            f"{self._key_data_type.convert(str(k))}={self._value_data_type.convert(str(v))}"
610            for k, v in self.value.items()
611        ]
612        return ",".join(items)

A config value that is a dictionary of string keys and values of type T.

Dict( default: dict[~KT, ~VT] = MISSING, *, key_type: BaseDataType[~KT] = MISSING, value_type: BaseDataType[~VT] = MISSING)
547    def __init__(
548        self,
549        default: dict[KT, VT] = UNSET,
550        *,
551        key_type: BaseDataType[KT] = UNSET,
552        value_type: BaseDataType[VT] = UNSET,
553    ) -> None:
554        """Initialize the dict data type."""
555        if default is UNSET and (key_type is UNSET or value_type is UNSET):
556            msg = "Dict requires either a default with at least one key/value pair, or both key_type and value_type to be specified."  # noqa: E501
557            raise InvalidDefaultError(msg)
558        if default is UNSET:
559            default = {}
560        super().__init__(default)
561
562        self._infer_key_type(default, key_type)
563        self._infer_value_type(default, value_type)

Initialize the dict data type.

def convert(self, value: str) -> dict[~KT, ~VT]:
589    def convert(self, value: str) -> dict[KT, VT]:
590        """Convert a string to a dictionary."""
591        if not value:
592            return {}
593
594        parts = value.split(",")
595        result: dict[KT, VT] = {}
596        for part in parts:
597            if "=" not in part:
598                msg = f"Invalid dictionary entry: {part}. Expected format key=value."
599                raise ValueError(msg)
600            key_str, val_str = part.split("=", 1)
601            key = self._key_data_type.convert(key_str.strip())
602            val = self._value_data_type.convert(val_str.strip())
603            result[key] = val
604        return result

Convert a string to a dictionary.

class Enum(confkit.data_types._EnumBase[~EnumType]):
128class Enum(_EnumBase[EnumType]):
129    """A config value that is an enum."""
130
131    def convert(self, value: str) -> EnumType:
132        """Convert a string value to an enum."""
133        value = self._strip_comment(value)
134        parsed_enum_name = value.split(".")[-1]
135        return self.value.__class__[parsed_enum_name]
136
137    def _format_allowed_values(self) -> str:
138        """Format allowed values as comma-separated member names."""
139        enum_class = self.value.__class__
140        return ", ".join(member.name for member in enum_class)
141
142    def _get_value_str(self) -> str:
143        """Get the member name."""
144        return self.value.name

A config value that is an enum.

def convert(self, value: str) -> ~EnumType:
131    def convert(self, value: str) -> EnumType:
132        """Convert a string value to an enum."""
133        value = self._strip_comment(value)
134        parsed_enum_name = value.split(".")[-1]
135        return self.value.__class__[parsed_enum_name]

Convert a string value to an enum.

class Float(confkit.BaseDataType[float]):
232class Float(BaseDataType[float]):
233    """A config value that is a float."""
234
235    def __init__(self, default: float = 0.0) -> None:  # noqa: D107
236        super().__init__(default)
237
238    def convert(self, value: str) -> float:
239        """Convert a string value to a float."""
240        return float(value)

A config value that is a float.

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

Initialize the base data type.

def convert(self, value: str) -> float:
238    def convert(self, value: str) -> float:
239        """Convert a string value to a float."""
240        return float(value)

Convert a string value to a float.

class Hex(confkit.BaseDataType[int]):
301class Hex(Integer):
302    """A config value that represents hexadecimal."""
303
304    def __init__(self, default: int = 0, base: int = HEXADECIMAL) -> None:  # noqa: D107
305        super().__init__(default, base)
306
307    def __str__(self) -> str:  # noqa: D105
308        return f"0x{self.value:x}"
309
310    def convert(self, value: str) -> int:
311        """Convert a string value to an integer. from hexadecimal."""
312        return int(value.removeprefix("0x"), 16)

A config value that represents hexadecimal.

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

Initialize the base data type.

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

Convert a string value to an integer. from hexadecimal.

class IntEnum(confkit.data_types._EnumBase[~IntEnumType]):
165class IntEnum(_EnumBase[IntEnumType]):
166    """A config value that is an enum."""
167
168    def convert(self, value: str) -> IntEnumType:
169        """Convert a string value to an enum."""
170        value = self._strip_comment(value)
171        return self.value.__class__(int(value))
172
173    def _format_allowed_values(self) -> str:
174        """Format allowed values as comma-separated name(value) pairs."""
175        enum_class = self.value.__class__
176        return ", ".join(f"{member.name}({member.value})" for member in enum_class)
177
178    def _get_value_str(self) -> str:
179        """Get the member value as string."""
180        return str(self.value.value)

A config value that is an enum.

def convert(self, value: str) -> ~IntEnumType:
168    def convert(self, value: str) -> IntEnumType:
169        """Convert a string value to an enum."""
170        value = self._strip_comment(value)
171        return self.value.__class__(int(value))

Convert a string value to an enum.

class IntFlag(confkit.data_types._EnumBase[~IntFlagType]):
183class IntFlag(_EnumBase[IntFlagType]):
184    """A config value that is an enum."""
185
186    def convert(self, value: str) -> IntFlagType:
187        """Convert a string value to an enum."""
188        value = self._strip_comment(value)
189        return self.value.__class__(int(value))
190
191    def _format_allowed_values(self) -> str:
192        """Format allowed values as comma-separated name(value) pairs."""
193        enum_class = self.value.__class__
194        return ", ".join(f"{member.name}({member.value})" for member in enum_class)
195
196    def _get_value_str(self) -> str:
197        """Get the member value as string."""
198        return str(self.value.value)

A config value that is an enum.

def convert(self, value: str) -> ~IntFlagType:
186    def convert(self, value: str) -> IntFlagType:
187        """Convert a string value to an enum."""
188        value = self._strip_comment(value)
189        return self.value.__class__(int(value))

Convert a string value to an enum.

class Integer(confkit.BaseDataType[int]):
263class Integer(BaseDataType[int]):
264    """A config value that is an integer."""
265
266    # Define constants for common bases
267
268    def __init__(self, default: int = 0, base: int = DECIMAL) -> None:  # noqa: D107
269        super().__init__(default)
270        self.base = base
271
272    @staticmethod
273    def int_to_base(number: int, base: int) -> int:
274        """Convert an integer to a string representation in a given base."""
275        if number == 0:
276            return 0
277        digits = []
278        while number:
279            digits.append(str(number % base))
280            number //= base
281        return int("".join(reversed(digits)))
282
283    def __str__(self) -> str:  # noqa: D105
284        if self.base == DECIMAL:
285            return str(self.value)
286        # Convert the base 10 int to base 5
287        self.value = self.int_to_base(int(self.value), self.base)
288        return f"{self.base}c{self.value}"
289
290    def convert(self, value: str) -> int:
291        """Convert a string value to an integer."""
292        if "c" in value:
293            base_str, val_str = value.split("c")
294            base = int(base_str)
295            if base != self.base:
296                msg = "Base in string does not match base in Integer while converting."
297                raise ValueError(msg)
298            return int(val_str, self.base)
299        return int(value, self.base)

A config value that is an integer.

Integer(default: int = 0, base: int = 10)
268    def __init__(self, default: int = 0, base: int = DECIMAL) -> None:  # noqa: D107
269        super().__init__(default)
270        self.base = base

Initialize the base data type.

base
@staticmethod
def int_to_base(number: int, base: int) -> int:
272    @staticmethod
273    def int_to_base(number: int, base: int) -> int:
274        """Convert an integer to a string representation in a given base."""
275        if number == 0:
276            return 0
277        digits = []
278        while number:
279            digits.append(str(number % base))
280            number //= base
281        return int("".join(reversed(digits)))

Convert an integer to a string representation in a given base.

def convert(self, value: str) -> int:
290    def convert(self, value: str) -> int:
291        """Convert a string value to an integer."""
292        if "c" in value:
293            base_str, val_str = value.split("c")
294            base = int(base_str)
295            if base != self.base:
296                msg = "Base in string does not match base in Integer while converting."
297                raise ValueError(msg)
298            return int(val_str, self.base)
299        return int(value, self.base)

Convert a string value to an integer.

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

Raised when the converter is not set or invalid.

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

Raised when the default value is not set or invalid.

class List(confkit.data_types._SequenceType[~T], typing.Generic[~T]):
468class List(_SequenceType[T], Generic[T]):
469    """A config value that is a list of values."""
470
471    def convert(self, value: str) -> list[T]:
472        """Convert a string to a list."""
473        return list(super()._convert(value))

A config value that is a list of values.

def convert(self, value: str) -> list[~T]:
471    def convert(self, value: str) -> list[T]:
472        """Convert a string to a list."""
473        return list(super()._convert(value))

Convert a string to a list.

class NoneType(confkit.BaseDataType[NoneType]):
200class NoneType(BaseDataType[None]):
201    """A config value that is None."""
202
203    null_values: ClassVar[set[str]] = {"none", "null", "nil"}
204
205    def __init__(self) -> None:
206        """Initialize the NoneType data type."""
207        super().__init__(None)
208
209    def is_valid(self, value: str) -> bool:
210        """Check if the provided string value is in the set of null values."""
211        return value.casefold().strip() in NoneType.null_values
212
213    def convert(self, value: str) -> None:
214        """Convert a string value to None."""
215        if self.is_valid(value):
216            return
217        msg = f"Value '{value}' is not a valid null value. Expected one of: {', '.join(NoneType.null_values)}."
218        raise ValueError(msg)

A config value that is None.

NoneType()
205    def __init__(self) -> None:
206        """Initialize the NoneType data type."""
207        super().__init__(None)

Initialize the NoneType data type.

null_values: ClassVar[set[str]] = {'none', 'null', 'nil'}
def is_valid(self, value: str) -> bool:
209    def is_valid(self, value: str) -> bool:
210        """Check if the provided string value is in the set of null values."""
211        return value.casefold().strip() in NoneType.null_values

Check if the provided string value is in the set of null values.

def convert(self, value: str) -> None:
213    def convert(self, value: str) -> None:
214        """Convert a string value to None."""
215        if self.is_valid(value):
216            return
217        msg = f"Value '{value}' is not a valid null value. Expected one of: {', '.join(NoneType.null_values)}."
218        raise ValueError(msg)

Convert a string value to None.

class Octal(confkit.BaseDataType[int]):
314class Octal(Integer):
315    """A config value that represents octal."""
316
317    def __init__(self, default: int = 0, base: int = OCTAL) -> None:  # noqa: D107
318        super().__init__(default, base)
319
320    def __str__(self) -> str:  # noqa: D105
321        return f"0o{self.value:o}"
322
323    def convert(self, value: str) -> int:
324        """Convert a string value to an integer from octal."""
325        return int(value.removeprefix("0o"), 8)

A config value that represents octal.

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

Initialize the base data type.

def convert(self, value: str) -> int:
323    def convert(self, value: str) -> int:
324        """Convert a string value to an integer from octal."""
325        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]):
344class Optional(BaseDataType[T | None], Generic[T]):
345    """A config value that is optional, can be None or a specific type."""
346
347    _none_type = NoneType()
348
349    def __init__(self, data_type: BaseDataType[T]) -> None:
350        """Initialize the optional data type. Wrapping the provided data type."""
351        self._data_type = data_type
352
353    @property
354    def default(self) -> T | None:
355        """Get the default value of the wrapped data type."""
356        return self._data_type.default
357
358    @property
359    def value(self) -> T | None:
360        """Get the current value of the wrapped data type."""
361        return self._data_type.value
362
363    @value.setter
364    def value(self, value: T | None) -> None:
365        """Set the current value of the wrapped data type."""
366        self._data_type.value = value
367
368    def convert(self, value: str) -> T | None:
369        """Convert a string value to the optional type."""
370        if self._none_type.is_valid(value):
371            return self._none_type.convert(value)
372        return self._data_type.convert(value)
373
374    def validate(self) -> bool:
375        """Validate that the value is of the wrapped data type or None."""
376        if self._data_type.value is None:
377            return True
378        return self._data_type.validate()
379
380    def __str__(self) -> str:
381        """Return the string representation of the wrapped data type."""
382        return str(self._data_type)

A config value that is optional, can be None or a specific type.

Optional(data_type: BaseDataType[~T])
349    def __init__(self, data_type: BaseDataType[T]) -> None:
350        """Initialize the optional data type. Wrapping the provided data type."""
351        self._data_type = data_type

Initialize the optional data type. Wrapping the provided data type.

default: Optional[~T]
353    @property
354    def default(self) -> T | None:
355        """Get the default value of the wrapped data type."""
356        return self._data_type.default

Get the default value of the wrapped data type.

value: Optional[~T]
358    @property
359    def value(self) -> T | None:
360        """Get the current value of the wrapped data type."""
361        return self._data_type.value

Get the current value of the wrapped data type.

def convert(self, value: str) -> Optional[~T]:
368    def convert(self, value: str) -> T | None:
369        """Convert a string value to the optional type."""
370        if self._none_type.is_valid(value):
371            return self._none_type.convert(value)
372        return self._data_type.convert(value)

Convert a string value to the optional type.

def validate(self) -> bool:
374    def validate(self) -> bool:
375        """Validate that the value is of the wrapped data type or None."""
376        if self._data_type.value is None:
377            return True
378        return self._data_type.validate()

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

class Set(confkit.BaseDataType[set[~T]], typing.Generic[~T]):
482class Set(BaseDataType[set[T]], Generic[T]):
483    """A config value that is a set of values."""
484
485    @overload
486    def __init__(self, default: set[T]) -> None: ...
487    @overload
488    def __init__(self, *, data_type: BaseDataType[T]) -> None: ...
489    @overload
490    def __init__(
491        self,
492        default: set[T],
493        *,
494        data_type: BaseDataType[T] = ...,
495    ) -> None: ...
496
497    def __init__(self, default: set[T] = UNSET, *, data_type: BaseDataType[T] = UNSET) -> None:
498        """Initialize the set data type."""
499        if default is UNSET and data_type is UNSET:
500            msg = "Set requires either a default with at least one element, or data_type to be specified."
501            raise InvalidDefaultError(msg)
502        if default is UNSET:
503            default = set()
504        super().__init__(default)
505        self._infer_type(default, data_type)
506
507    def _infer_type(self, default: set[T], data_type: BaseDataType[T]) -> None:
508        if len(default) <= 0 and data_type is UNSET:
509            msg = "Set default must have at least one element to infer type. or specify `data_type=<BaseDataType>`"
510            raise InvalidDefaultError(msg)
511        if data_type is UNSET:
512            sample_element = default.pop()
513            default.add(sample_element)
514            self._data_type = BaseDataType[T].cast(sample_element)
515        else:
516            self._data_type = data_type
517
518    def convert(self, value: str) -> set[T]:
519        """Convert a string to a set."""
520        if not value:
521            return set()
522        parts = value.split(",")
523        return {self._data_type.convert(item.strip()) for item in parts}
524
525    def __str__(self) -> str:
526        """Return a string representation of the set."""
527        return ",".join(str(item) for item in self.value)

A config value that is a set of values.

Set( default: set[~T] = MISSING, *, data_type: BaseDataType[~T] = MISSING)
497    def __init__(self, default: set[T] = UNSET, *, data_type: BaseDataType[T] = UNSET) -> None:
498        """Initialize the set data type."""
499        if default is UNSET and data_type is UNSET:
500            msg = "Set requires either a default with at least one element, or data_type to be specified."
501            raise InvalidDefaultError(msg)
502        if default is UNSET:
503            default = set()
504        super().__init__(default)
505        self._infer_type(default, data_type)

Initialize the set data type.

def convert(self, value: str) -> set[~T]:
518    def convert(self, value: str) -> set[T]:
519        """Convert a string to a set."""
520        if not value:
521            return set()
522        parts = value.split(",")
523        return {self._data_type.convert(item.strip()) for item in parts}

Convert a string to a set.

class StrEnum(confkit.data_types._EnumBase[~StrEnumType]):
147class StrEnum(_EnumBase[StrEnumType]):
148    """A config value that is an enum."""
149
150    def convert(self, value: str) -> StrEnumType:
151        """Convert a string value to an enum."""
152        value = self._strip_comment(value)
153        return self.value.__class__(value)
154
155    def _format_allowed_values(self) -> str:
156        """Format allowed values as comma-separated member values."""
157        enum_class = self.value.__class__
158        return ", ".join(member.value for member in enum_class)
159
160    def _get_value_str(self) -> str:
161        """Get the member value."""
162        return self.value.value

A config value that is an enum.

def convert(self, value: str) -> ~StrEnumType:
150    def convert(self, value: str) -> StrEnumType:
151        """Convert a string value to an enum."""
152        value = self._strip_comment(value)
153        return self.value.__class__(value)

Convert a string value to an enum.

class String(confkit.BaseDataType[str]):
221class String(BaseDataType[str]):
222    """A config value that is a string."""
223
224    def __init__(self, default: str = "") -> None:  # noqa: D107
225        super().__init__(default)
226
227    def convert(self, value: str) -> str:
228        """Convert a string value to a string."""
229        return value

A config value that is a string.

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

Initialize the base data type.

def convert(self, value: str) -> str:
227    def convert(self, value: str) -> str:
228        """Convert a string value to a string."""
229        return value

Convert a string value to a string.

class Time(confkit.BaseDataType[datetime.time]):
684class Time(BaseDataType[time]):
685    """A config value that is a time."""
686
687    @overload
688    def __init__(self, default: time = UNSET) -> None: ...
689    @overload
690    def __init__(self, **kwargs: Unpack[_TimeKwargs]) -> None: ...
691
692    def __init__(self, default: time = UNSET, **kwargs: Unpack[_TimeKwargs]) -> None:
693        """Initialize the time data type. Defaults to current time if not provided."""
694        if default is UNSET:
695            default = time(**kwargs)
696        super().__init__(default)
697
698    def convert(self, value: str) -> time:
699        """Convert a string value to a time."""
700        return time.fromisoformat(value)
701
702    def __str__(self) -> str:  # noqa: D105
703        return self.value.isoformat()

A config value that is a time.

Time( default: datetime.time = MISSING, **kwargs: *<class 'confkit.data_types._TimeKwargs'>)
692    def __init__(self, default: time = UNSET, **kwargs: Unpack[_TimeKwargs]) -> None:
693        """Initialize the time data type. Defaults to current time if not provided."""
694        if default is UNSET:
695            default = time(**kwargs)
696        super().__init__(default)

Initialize the time data type. Defaults to current time if not provided.

def convert(self, value: str) -> datetime.time:
698    def convert(self, value: str) -> time:
699        """Convert a string value to a time."""
700        return time.fromisoformat(value)

Convert a string value to a time.

class TimeDelta(confkit.BaseDataType[datetime.timedelta]):
714class TimeDelta(BaseDataType[timedelta]):
715    """A config value that is a timedelta."""
716
717    def __init__(
718        self,
719        default: timedelta = UNSET,
720        **kwargs: Unpack[_TimeDeltaKwargs],
721    ) -> None:
722        """Initialize the timedelta data type. Defaults to 0 if not provided."""
723        if default is UNSET:
724            default = timedelta(**kwargs)
725        super().__init__(default)
726
727    def convert(self, value: str) -> timedelta:
728        """Convert a string value to a timedelta."""
729        return timedelta(seconds=float(value))
730
731    def __str__(self) -> str:  # noqa: D105
732        return str(self.value.total_seconds())

A config value that is a timedelta.

TimeDelta( default: datetime.timedelta = MISSING, **kwargs: *<class 'confkit.data_types._TimeDeltaKwargs'>)
717    def __init__(
718        self,
719        default: timedelta = UNSET,
720        **kwargs: Unpack[_TimeDeltaKwargs],
721    ) -> None:
722        """Initialize the timedelta data type. Defaults to 0 if not provided."""
723        if default is UNSET:
724            default = timedelta(**kwargs)
725        super().__init__(default)

Initialize the timedelta data type. Defaults to 0 if not provided.

def convert(self, value: str) -> datetime.timedelta:
727    def convert(self, value: str) -> timedelta:
728        """Convert a string value to a timedelta."""
729        return timedelta(seconds=float(value))

Convert a string value to a timedelta.

class Tuple(confkit.data_types._SequenceType[~T], typing.Generic[~T]):
475class Tuple(_SequenceType[T], Generic[T]):
476    """A config value that is a tuple of values."""
477
478    def convert(self, value: str) -> tuple[T, ...]:
479        """Convert a string to a tuple."""
480        return tuple(super()._convert(value))

A config value that is a tuple of values.

def convert(self, value: str) -> tuple[~T, ...]:
478    def convert(self, value: str) -> tuple[T, ...]:
479        """Convert a string to a tuple."""
480        return tuple(super()._convert(value))

Convert a string to a tuple.