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

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

BaseDataType(default: ~T)
22    def __init__(self, default: T) -> None:
23        """Initialize the base data type."""
24        self.default = default
25        self.value = default
26        self.type = type(default)

Initialize the base data type.

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

Convert a string value to the desired type.

def validate(self) -> bool:
36    def validate(self) -> bool:
37        """Validate that the value matches the expected type."""
38        orig_bases: tuple[type, ...] | None = getattr(self.__class__, "__orig_bases__", None)
39
40        if not orig_bases:
41            msg = "No type information available for validation."
42            raise InvalidConverterError(msg)
43
44        # Extract type arguments from the generic base
45        for base in orig_bases:
46            if hasattr(base, "__args__"):
47                type_args = base.__args__
48                if type_args:
49                    for type_arg in type_args:
50                        if hasattr(type_arg, "__origin__"):
51                            # For parameterized generics, check against the origin type
52                            if isinstance(self.value, type_arg.__origin__):
53                                return True
54                        elif isinstance(self.value, (self.type, type_arg)):
55                            return True
56                    msg = f"Value {self.value} is not any of {type_args}."
57                    raise InvalidConverterError(msg)
58        msg = "This should not have raised. Report to the library maintainers with code: `DTBDT`"
59        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]]:
61    @staticmethod
62    def cast_optional(default: T | None | BaseDataType[T]) -> BaseDataType[T | None]:
63        """Convert the default value to an Optional data type."""
64        if default is None:
65            return cast("BaseDataType[T | None]", NoneType())
66        return Optional(BaseDataType.cast(default))

Convert the default value to an Optional data type.

@staticmethod
def cast( default: Union[~T, BaseDataType[~T]]) -> BaseDataType[~T]:
68    @staticmethod
69    def cast(default: T | BaseDataType[T]) -> BaseDataType[T]:
70        """Convert the default value to a BaseDataType."""
71        # We use Cast to shut up type checkers, as we know primitive types will be correct.
72        # If a custom type is passed, it should be a BaseDataType subclass, which already has the correct types.
73        match default:
74            case bool():
75                data_type = cast("BaseDataType[T]", Boolean(default))
76            case None:
77                data_type = cast("BaseDataType[T]", NoneType())
78            case int():
79                data_type = cast("BaseDataType[T]", Integer(default))
80            case float():
81                data_type = cast("BaseDataType[T]", Float(default))
82            case str():
83                data_type = cast("BaseDataType[T]", String(default))
84            case BaseDataType():
85                data_type = default
86            case _:
87                msg = (
88                    f"Unsupported default value type: {type(default).__name__}. "
89                    "Use a BaseDataType subclass for custom types."
90                )
91                raise InvalidDefaultError(msg)
92        return data_type

Convert the default value to a BaseDataType.

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

A config value that represents binary.

Binary(default: bytes | int = 0)
323    def __init__(self, default: bytes | int = 0) -> None:  # noqa: D107
324        if isinstance(default, bytes):
325            default = int.from_bytes(default)
326        super().__init__(default)

Initialize the base data type.

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

Convert a string value to an integer from binary.

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

A config value that is a boolean.

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

Initialize the base data type.

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

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)
 81    def __init__(
 82        self,
 83        default: VT | None | BaseDataType[VT] = UNSET,
 84        *,
 85        optional: bool = False,
 86    ) -> None:
 87        """Initialize the config descriptor with a default value.
 88
 89        Validate that parser and filepath are present.
 90        """
 91        cls = self.__class__
 92        self.optional = optional or cls.optional # Be truthy when either one is true.
 93
 94        if not self.optional and default is UNSET:
 95            msg = "Default value cannot be None when optional is False."
 96            raise InvalidDefaultError(msg)
 97
 98        if not self._parser:
 99            self._detect_parser()
100
101        self._initialize_data_type(default)
102        self._validate_init()
103        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:
146    def convert(self, value: str) -> VT:
147        """Convert the value to the desired type using the given converter method."""
148        # Ignore the type error of VT, type checkers don't like None as an option
149        # We handle it using the `optional` flag, or using Optional DataType. so we can safely ignore it.
150        return self._data_type.convert(value) # type: ignore[reportReturnType]

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:
159    @classmethod
160    @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
161    def set_parser(cls, parser: ConfkitParser) -> None:
162        """Set the parser for ALL descriptor instances (of this type/class)."""
163        if cls is Config:
164            cls._warn_base_class_usage()
165        cls._parser = parser

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

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

Set the file for ALL descriptors.

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

Validate the type of the converter matches the desired type.

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

Validate the config file.

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

Validate the config parser.

@classmethod
def write(cls) -> None:
316    @classmethod
317    def write(cls) -> None:
318        """Write the config parser to the file."""
319        cls.validate_file()
320        with cls._file.open("w") as f:
321            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]]:
323    @classmethod
324    def set(cls, section: str, setting: str, value: VT) -> Callable[[Callable[P, F]], Callable[P, F]]:
325        """Set a config value using this descriptor."""
326
327        def wrapper(func: Callable[P, F]) -> Callable[P, F]:
328            @wraps(func)
329            def inner(*args: P.args, **kwargs: P.kwargs) -> F:
330                cls._set(section, setting, value)
331                return func(*args, **kwargs)
332
333            return inner
334        return wrapper

Set a config value using this descriptor.

@classmethod
def with_setting( cls, setting: Config[~OVT]) -> Callable[[Callable[~P, ~F]], Callable[~P, ~F]]:
337    @classmethod
338    def with_setting(cls, setting: Config[OVT]) -> Callable[[Callable[P, F]], Callable[P, F]]:
339        """Insert a config value into **kwargs to the wrapped method/function using this decorator."""
340        def wrapper(func: Callable[P, F]) -> Callable[P, F]:
341            @wraps(func)
342            def inner(*args: P.args, **kwargs: P.kwargs) -> F:
343                kwargs[setting.name] = setting.convert(cls._parser.get(setting._section, setting._setting))
344                return func(*args, **kwargs)
345
346            return inner
347        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]]:
350    @classmethod
351    def with_kwarg(
352        cls, section: str, setting: str, name: str | None = None, default: VT = UNSET,
353    ) -> Callable[[Callable[P, F]], Callable[P, F]]:
354        """Insert a config value into **kwargs to the wrapped method/function using this descriptor.
355
356        Use kwarg.get(`name`) to get the value.
357        `name` is the name the kwarg gets if passed, if None, it will be the same as `setting`.
358        Section parameter is just for finding the config value.
359        """
360        if name is None:
361            name = setting
362        if default is UNSET and not cls._parser.has_option(section, setting):
363            msg = f"Config value {section=} {setting=} is not set. and no default value is given."
364            raise ValueError(msg)
365
366        def wrapper(func: Callable[P, F]) -> Callable[P, F]:
367            @wraps(func)
368            def inner(*args: P.args, **kwargs: P.kwargs) -> F:
369                if default is not UNSET:
370                    cls._set_default(section, setting, default)
371                kwargs[name] = cls._parser.get(section, setting)
372                return func(*args, **kwargs)
373
374            return inner
375        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]]:
382    @classmethod
383    def default(cls, section: str, setting: str, value: VT) -> Callable[[Callable[P, F]], Callable[P, F]]:
384        """Set a default config value if none are set yet using this descriptor."""
385        def wrapper(func: Callable[P, F]) -> Callable[P, F]:
386            @wraps(func)
387            def inner(*args: P.args, **kwargs: P.kwargs) -> F:
388                cls._set_default(section, setting, value)
389                return func(*args, **kwargs)
390
391            return inner
392        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:
394    @abstractmethod
395    def on_file_change(self, origin: Literal["get", "set"], old: VT | UNSET, new: VT) -> None:
396        """Triggered when the config file changes.
397
398        This needs to be implemented before it's usable.
399        This will be called **before** setting the value from the config file.
400        This will be called **after** getting (but before validating it's type) the value from config file.
401        The `origin` parameter indicates whether the change was triggered by a `get` or `set` operation.
402        """

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):
12class ConfigPathConflictError(ValueError):
13    """Raised when a configuration path conflicts with an existing scalar value.
14
15    This occurs when attempting to treat a scalar value as a section (dict).
16    For example, if "Parent.Value" is a scalar, attempting to set "Parent.Value.Child"
17    would cause this error.
18    """

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]):
648class Date(BaseDataType[date]):
649    """A config value that is a date."""
650
651    @overload
652    def __init__(self, default: date = UNSET) -> None: ...
653    @overload
654    def __init__(self, **kwargs: Unpack[_DateKwargs]) -> None: ...
655
656    def __init__(self, default: date = UNSET, **kwargs: Unpack[_DateKwargs]) -> None:
657        """Initialize the date data type. Defaults to current date if not provided."""
658        if default is UNSET:
659            default = date(**kwargs)
660        super().__init__(default)
661
662    def convert(self, value: str) -> date:
663        """Convert a string value to a date."""
664        return date.fromisoformat(value)
665
666    def __str__(self) -> str:  # noqa: D105
667        return self.value.isoformat()

A config value that is a date.

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

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

def convert(self, value: str) -> datetime.date:
662    def convert(self, value: str) -> date:
663        """Convert a string value to a date."""
664        return date.fromisoformat(value)

Convert a string value to a date.

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

A config value that is a datetime.

DateTime( default: datetime.datetime = MISSING, **kwargs: *<class 'confkit.data_types._DateTimeKwargs'>)
626    def __init__(self, default: datetime = UNSET, **kwargs: Unpack[_DateTimeKwargs]) -> None:
627        """Initialize the datetime data type. Defaults to current datetime (datetime.now) if not provided."""
628        if default is UNSET:
629            try:
630                default = datetime(**kwargs)  # noqa: DTZ001 Tzinfo is (optionally) passed using kwargs
631            except TypeError:
632                default = datetime.now(tz=UTC)
633        super().__init__(default)

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

def convert(self, value: str) -> datetime.datetime:
635    def convert(self, value: str) -> datetime:
636        """Convert a string value to a datetime."""
637        return datetime.fromisoformat(value)

Convert a string value to a datetime.

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

Initialize the dict data type.

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

Convert a string to a dictionary.

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

A config value that is an enum.

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

Convert a string value to an enum.

class Float(confkit.BaseDataType[float]):
225class Float(BaseDataType[float]):
226    """A config value that is a float."""
227
228    def __init__(self, default: float = 0.0) -> None:  # noqa: D107
229        super().__init__(default)
230
231    def convert(self, value: str) -> float:
232        """Convert a string value to a float."""
233        return float(value)

A config value that is a float.

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

Initialize the base data type.

def convert(self, value: str) -> float:
231    def convert(self, value: str) -> float:
232        """Convert a string value to a float."""
233        return float(value)

Convert a string value to a float.

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

A config value that represents hexadecimal.

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

Initialize the base data type.

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

Convert a string value to an integer. from hexadecimal.

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

A config value that is an enum.

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

Convert a string value to an enum.

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

A config value that is an enum.

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

Convert a string value to an enum.

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

A config value that is an integer.

Integer(default: int = 0, base: int = 10)
261    def __init__(self, default: int = 0, base: int = DECIMAL) -> None:  # noqa: D107
262        super().__init__(default)
263        self.base = base

Initialize the base data type.

base
@staticmethod
def int_to_base(number: int, base: int) -> int:
265    @staticmethod
266    def int_to_base(number: int, base: int) -> int:
267        """Convert an integer to a string representation in a given base."""
268        if number == 0:
269            return 0
270        digits = []
271        while number:
272            digits.append(str(number % base))
273            number //= base
274        return int("".join(reversed(digits)))

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

def convert(self, value: str) -> int:
283    def convert(self, value: str) -> int:
284        """Convert a string value to an integer."""
285        if "c" in value:
286            base_str, val_str = value.split("c")
287            base = int(base_str)
288            if base != self.base:
289                msg = "Base in string does not match base in Integer while converting."
290                raise ValueError(msg)
291            return int(val_str, self.base)
292        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.data_types._SequenceType[~T], typing.Generic[~T]):
461class List(_SequenceType[T], Generic[T]):
462    """A config value that is a list of values."""
463
464    def convert(self, value: str) -> list[T]:
465        """Convert a string to a list."""
466        return list(super()._convert(value))

A config value that is a list of values.

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

Convert a string to a list.

class NoneType(confkit.BaseDataType[NoneType]):
198class NoneType(BaseDataType[None]):
199    """A config value that is None."""
200
201    null_values: ClassVar[set[str]] = {"none", "null", "nil"}
202
203    def __init__(self) -> None:
204        """Initialize the NoneType data type."""
205        super().__init__(None)
206
207    def convert(self, value: str) -> bool: # type: ignore[reportIncompatibleMethodOverride]
208        """Convert a string value to None."""
209        # Ignore type exception as convert should return True/False for NoneType
210        # to determine if we have a valid null value or not.
211        return value.casefold().strip() in NoneType.null_values

A config value that is None.

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

Initialize the NoneType data type.

null_values: ClassVar[set[str]] = {'null', 'nil', 'none'}
def convert(self, value: str) -> bool:
207    def convert(self, value: str) -> bool: # type: ignore[reportIncompatibleMethodOverride]
208        """Convert a string value to None."""
209        # Ignore type exception as convert should return True/False for NoneType
210        # to determine if we have a valid null value or not.
211        return value.casefold().strip() in NoneType.null_values

Convert a string value to None.

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

A config value that represents octal.

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

Initialize the base data type.

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

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

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

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

default: Optional[~T]
346    @property
347    def default(self) -> T | None:
348        """Get the default value of the wrapped data type."""
349        return self._data_type.default

Get the default value of the wrapped data type.

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

Get the current value of the wrapped data type.

def convert(self, value: str) -> Optional[~T]:
361    def convert(self, value: str) -> T | None:
362        """Convert a string value to the optional type."""
363        if self._none_type.convert(value):
364            return None
365        return self._data_type.convert(value)

Convert a string value to the optional type.

def validate(self) -> bool:
367    def validate(self) -> bool:
368        """Validate that the value is of the wrapped data type or None."""
369        if self._data_type.value is None:
370            return True
371        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]):
475class Set(BaseDataType[set[T]], Generic[T]):
476    """A config value that is a set of values."""
477
478    @overload
479    def __init__(self, default: set[T]) -> None: ...
480    @overload
481    def __init__(self, *, data_type: BaseDataType[T]) -> None: ...
482    @overload
483    def __init__(
484        self,
485        default: set[T],
486        *,
487        data_type: BaseDataType[T] = ...,
488    ) -> None: ...
489
490    def __init__(self, default: set[T] = UNSET, *, data_type: BaseDataType[T] = UNSET) -> None:
491        """Initialize the set data type."""
492        if default is UNSET and data_type is UNSET:
493            msg = "Set requires either a default with at least one element, or data_type to be specified."
494            raise InvalidDefaultError(msg)
495        if default is UNSET:
496            default = set()
497        super().__init__(default)
498        self._infer_type(default, data_type)
499
500    def _infer_type(self, default: set[T], data_type: BaseDataType[T]) -> None:
501        if len(default) <= 0 and data_type is UNSET:
502            msg = "Set default must have at least one element to infer type. or specify `data_type=<BaseDataType>`"
503            raise InvalidDefaultError(msg)
504        if data_type is UNSET:
505            sample_element = default.pop()
506            default.add(sample_element)
507            self._data_type = BaseDataType[T].cast(sample_element)
508        else:
509            self._data_type = data_type
510
511    def convert(self, value: str) -> set[T]:
512        """Convert a string to a set."""
513        if not value:
514            return set()
515        parts = value.split(",")
516        return {self._data_type.convert(item.strip()) for item in parts}
517
518    def __str__(self) -> str:
519        """Return a string representation of the set."""
520        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)
490    def __init__(self, default: set[T] = UNSET, *, data_type: BaseDataType[T] = UNSET) -> None:
491        """Initialize the set data type."""
492        if default is UNSET and data_type is UNSET:
493            msg = "Set requires either a default with at least one element, or data_type to be specified."
494            raise InvalidDefaultError(msg)
495        if default is UNSET:
496            default = set()
497        super().__init__(default)
498        self._infer_type(default, data_type)

Initialize the set data type.

def convert(self, value: str) -> set[~T]:
511    def convert(self, value: str) -> set[T]:
512        """Convert a string to a set."""
513        if not value:
514            return set()
515        parts = value.split(",")
516        return {self._data_type.convert(item.strip()) for item in parts}

Convert a string to a set.

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

A config value that is an enum.

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

Convert a string value to an enum.

class String(confkit.BaseDataType[str]):
214class String(BaseDataType[str]):
215    """A config value that is a string."""
216
217    def __init__(self, default: str = "") -> None:  # noqa: D107
218        super().__init__(default)
219
220    def convert(self, value: str) -> str:
221        """Convert a string value to a string."""
222        return value

A config value that is a string.

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

Initialize the base data type.

def convert(self, value: str) -> str:
220    def convert(self, value: str) -> str:
221        """Convert a string value to a string."""
222        return value

Convert a string value to a string.

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

A config value that is a time.

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

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

def convert(self, value: str) -> datetime.time:
691    def convert(self, value: str) -> time:
692        """Convert a string value to a time."""
693        return time.fromisoformat(value)

Convert a string value to a time.

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

A config value that is a timedelta.

TimeDelta( default: datetime.timedelta = MISSING, **kwargs: *<class 'confkit.data_types._TimeDeltaKwargs'>)
710    def __init__(
711        self,
712        default: timedelta = UNSET,
713        **kwargs: Unpack[_TimeDeltaKwargs],
714    ) -> None:
715        """Initialize the timedelta data type. Defaults to 0 if not provided."""
716        if default is UNSET:
717            default = timedelta(**kwargs)
718        super().__init__(default)

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

def convert(self, value: str) -> datetime.timedelta:
720    def convert(self, value: str) -> timedelta:
721        """Convert a string value to a timedelta."""
722        return timedelta(seconds=float(value))

Convert a string value to a timedelta.

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

A config value that is a tuple of values.

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

Convert a string to a tuple.