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

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

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

@classmethod
def set_parser(cls, parser: confkit.ext.parsers.ConfkitParser) -> None:
156    @classmethod
157    def set_parser(cls, parser: ConfkitParser) -> None:
158        """Set the parser for ALL descriptors."""
159        if cls is Config:
160            cls._warn_base_class_usage()
161        cls._parser = parser

Set the parser for ALL descriptors.

@classmethod
def set_file(cls, file: pathlib.Path) -> None:
183    @classmethod
184    def set_file(cls, file: Path) -> None:
185        """Set the file for ALL descriptors."""
186        if cls is Config:
187            cls._warn_base_class_usage()
188        cls._file = file
189        cls._watcher = FileWatcher(file)

Set the file for ALL descriptors.

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

Validate the type of the converter matches the desired type.

@classmethod
def validate_file(cls) -> None:
221    @classmethod
222    def validate_file(cls) -> None:
223        """Validate the config file."""
224        if cls._file is UNSET:
225            msg = f"Config file is not set. use {cls.__name__}.set_file() to set it."
226            raise ValueError(msg)

Validate the config file.

@classmethod
def validate_parser(cls) -> None:
228    @classmethod
229    def validate_parser(cls) -> None:
230        """Validate the config parser."""
231        if cls._parser is UNSET:
232            msg = f"Config parser is not set. use {cls.__name__}.set_parser() to set it."
233            raise ValueError(msg)

Validate the config parser.

@classmethod
def write(cls) -> None:
300    @classmethod
301    def write(cls) -> None:
302        """Write the config parser to the file."""
303        cls.validate_file()
304        with cls._file.open("w") as f:
305            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]]:
307    @classmethod
308    def set(cls, section: str, setting: str, value: VT) -> Callable[[Callable[P, F]], Callable[P, F]]:
309        """Set a config value using this descriptor."""
310
311        def wrapper(func: Callable[P, F]) -> Callable[P, F]:
312            @wraps(func)
313            def inner(*args: P.args, **kwargs: P.kwargs) -> F:
314                cls._set(section, setting, value)
315                return func(*args, **kwargs)
316
317            return inner
318        return wrapper

Set a config value using this descriptor.

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

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

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

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.