confkit

Module that provides the main interface for the confkit package.

It includes the Config class and various data types used for configuration values.

 1"""Module that provides the main interface for the confkit package.
 2
 3It includes the Config class and various data types used for configuration values.
 4"""
 5from __future__ import annotations
 6
 7from .config import Config, ConfigContainerMeta
 8from .data_types import (
 9    BaseDataType,
10    Binary,
11    Boolean,
12    Date,
13    DateTime,
14    Dict,
15    Enum,
16    Float,
17    Hex,
18    Integer,
19    IntEnum,
20    IntFlag,
21    List,
22    NoneType,
23    Octal,
24    Optional,
25    Set,
26    StrEnum,
27    String,
28    Time,
29    TimeDelta,
30    Tuple,
31)
32from .exceptions import ConfigPathConflictError, InvalidConverterError, InvalidDefaultError
33
34__all__ = [
35    "BaseDataType",
36    "Binary",
37    "Boolean",
38    "Config",
39    "ConfigContainerMeta",
40    "ConfigPathConflictError",
41    "Date",
42    "DateTime",
43    "Dict",
44    "Enum",
45    "Float",
46    "Hex",
47    "IntEnum",
48    "IntFlag",
49    "Integer",
50    "InvalidConverterError",
51    "InvalidDefaultError",
52    "List",
53    "NoneType",
54    "Octal",
55    "Optional",
56    "Set",
57    "StrEnum",
58    "String",
59    "Time",
60    "TimeDelta",
61    "Tuple",
62]
class BaseDataType(abc.ABC, typing.Generic[~T]):
 27class BaseDataType(ABC, Generic[T]):
 28    """Base class used for Config descriptors to define a data type."""
 29
 30    def __init__(self, default: T) -> None:
 31        """Initialize the base data type."""
 32        self.default = default
 33        self.value = default
 34        self.type = type(default)
 35
 36    def __str__(self) -> str:
 37        """Return the string representation of the stored value."""
 38        return str(self.value)
 39
 40    @abstractmethod
 41    def convert(self, value: str) -> T:
 42        """Convert a string value to the desired type."""
 43        # effectively `return self.type(value)` should work for most types however,
 44        # we're marking this as abstract method to avoid validation and raising errors.
 45
 46    def validate(self) -> bool:
 47        """Validate that the value matches the expected type."""
 48        orig_bases: tuple[type, ...] | None = getattr(self.__class__, "__orig_bases__", None)
 49
 50        if not orig_bases:
 51            msg = "No type information available for validation."
 52            raise InvalidConverterError(msg)
 53
 54        # Extract type arguments from the generic base
 55        for base in orig_bases:
 56            if hasattr(base, "__args__"):
 57                type_args: list[type] = base.__args__
 58                if type_args:
 59                    for type_arg in type_args:
 60                        if hasattr(type_arg, "__origin__"):
 61                            # For parameterized generics, check against the origin type
 62                            origin: type = type_arg.__origin__
 63                            if isinstance(self.value, origin):
 64                                return True
 65                        elif isinstance(self.value, (self.type, type_arg)):
 66                            return True
 67                    msg = f"Value {self.value} is not any of {type_args}."
 68                    raise InvalidConverterError(msg)
 69        msg = "This should not have raised. Report to the library maintainers with code: `DTBDT`"
 70        raise TypeError(msg)
 71
 72    @staticmethod
 73    def cast_optional(default: T | None | BaseDataType[T]) -> BaseDataType[T | None]:
 74        """Convert the default value to an Optional data type."""
 75        if default is None:
 76            return cast("BaseDataType[T | None]", NoneType())
 77        return Optional(BaseDataType.cast(default))
 78
 79    @staticmethod
 80    def cast(default: T | BaseDataType[T]) -> BaseDataType[T]:  # noqa: C901, PLR0911, PLR0912
 81        """Convert the default value to a BaseDataType."""
 82        # We use Cast to shut up type checkers, as we know primitive types will be correct.
 83        # If a custom type is passed, it should be a BaseDataType subclass, which already has the correct types.
 84        # Check enum types BEFORE basic types since some enums inherit from str/int
 85        match default:
 86            case BaseDataType():                        return default
 87            case None:                                  return cast("BaseDataType[T]", NoneType())
 88            case dStrEnum():                            return cast("BaseDataType[T]", StrEnum(default))
 89            case dIntFlag():                            return cast("BaseDataType[T]", IntFlag(default))
 90            case dIntEnum():                            return cast("BaseDataType[T]", IntEnum(default))
 91            case dEnum():                               return cast("BaseDataType[T]", Enum(default))
 92            case bool():                                return cast("BaseDataType[T]", Boolean(default))
 93            case int():                                 return cast("BaseDataType[T]", Integer(default))
 94            case float():                               return cast("BaseDataType[T]", Float(default))
 95            case str():                                 return cast("BaseDataType[T]", String(default))
 96            case timedelta():                           return cast("BaseDataType[T]", TimeDelta(default))
 97            case datetime():                            return cast("BaseDataType[T]", DateTime(default))
 98            case time():                                return cast("BaseDataType[T]", Time(default))
 99            case date():                                return cast("BaseDataType[T]", Date(default))
100            case set():                                 return cast("BaseDataType[T]", Set(default))
101            case list():                                return cast("BaseDataType[T]", List(default))
102            case tuple():                               return cast("BaseDataType[T]", Tuple(default))
103            case PosixPath() | WindowsPath() | dPath(): return cast("BaseDataType[T]", Path(default))
104            case _:
105                msg = (
106                    f"Unsupported default value type: {type(default).__name__}. "
107                    "Use a BaseDataType subclass for custom types."
108                )
109                raise InvalidDefaultError(msg)

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

BaseDataType(default: ~T)
30    def __init__(self, default: T) -> None:
31        """Initialize the base data type."""
32        self.default = default
33        self.value = default
34        self.type = type(default)

Initialize the base data type.

default
value
type
@abstractmethod
def convert(self, value: str) -> ~T:
40    @abstractmethod
41    def convert(self, value: str) -> T:
42        """Convert a string value to the desired type."""
43        # effectively `return self.type(value)` should work for most types however,
44        # we're marking this as abstract method to avoid validation and raising errors.

Convert a string value to the desired type.

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

Convert the default value to an Optional data type.

@staticmethod
def cast( default: Union[~T, BaseDataType[~T]]) -> BaseDataType[~T]:
 79    @staticmethod
 80    def cast(default: T | BaseDataType[T]) -> BaseDataType[T]:  # noqa: C901, PLR0911, PLR0912
 81        """Convert the default value to a BaseDataType."""
 82        # We use Cast to shut up type checkers, as we know primitive types will be correct.
 83        # If a custom type is passed, it should be a BaseDataType subclass, which already has the correct types.
 84        # Check enum types BEFORE basic types since some enums inherit from str/int
 85        match default:
 86            case BaseDataType():                        return default
 87            case None:                                  return cast("BaseDataType[T]", NoneType())
 88            case dStrEnum():                            return cast("BaseDataType[T]", StrEnum(default))
 89            case dIntFlag():                            return cast("BaseDataType[T]", IntFlag(default))
 90            case dIntEnum():                            return cast("BaseDataType[T]", IntEnum(default))
 91            case dEnum():                               return cast("BaseDataType[T]", Enum(default))
 92            case bool():                                return cast("BaseDataType[T]", Boolean(default))
 93            case int():                                 return cast("BaseDataType[T]", Integer(default))
 94            case float():                               return cast("BaseDataType[T]", Float(default))
 95            case str():                                 return cast("BaseDataType[T]", String(default))
 96            case timedelta():                           return cast("BaseDataType[T]", TimeDelta(default))
 97            case datetime():                            return cast("BaseDataType[T]", DateTime(default))
 98            case time():                                return cast("BaseDataType[T]", Time(default))
 99            case date():                                return cast("BaseDataType[T]", Date(default))
100            case set():                                 return cast("BaseDataType[T]", Set(default))
101            case list():                                return cast("BaseDataType[T]", List(default))
102            case tuple():                               return cast("BaseDataType[T]", Tuple(default))
103            case PosixPath() | WindowsPath() | dPath(): return cast("BaseDataType[T]", Path(default))
104            case _:
105                msg = (
106                    f"Unsupported default value type: {type(default).__name__}. "
107                    "Use a BaseDataType subclass for custom types."
108                )
109                raise InvalidDefaultError(msg)

Convert the default value to a BaseDataType.

class Binary(confkit.BaseDataType[int]):
342class Binary(BaseDataType[int]):
343    """A config value that represents binary.
344
345    Accepts ``bytes`` or ``int`` on input; the value is always stored and returned as ``int``.
346    """
347
348    def __init__(self, default: bytes | int = 0) -> None:  # noqa: D107
349        if isinstance(default, bytes):
350            default = int.from_bytes(default)
351        super().__init__(default)
352
353    def __str__(self) -> str:  # noqa: D105
354        if isinstance(self.value, bytes):
355            self.value = int.from_bytes(self.value)
356        return f"0b{self.value:b}"
357
358    def convert(self, value: str) -> int:
359        """Convert a string value to an integer from binary."""
360        return int(value.removeprefix("0b"), 2)

A config value that represents binary.

Accepts bytes or int on input; the value is always stored and returned as int.

Binary(default: bytes | int = 0)
348    def __init__(self, default: bytes | int = 0) -> None:  # noqa: D107
349        if isinstance(default, bytes):
350            default = int.from_bytes(default)
351        super().__init__(default)

Initialize the base data type.

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

Convert a string value to an integer from binary.

class Boolean(confkit.BaseDataType[bool]):
258class Boolean(BaseDataType[bool]):
259    """A config value that is a boolean."""
260
261    def __init__(self, default: bool = False) -> None:  # noqa: D107, FBT001, FBT002
262        super().__init__(default)
263
264    def convert(self, value: str) -> bool:
265        """Convert a string value to a boolean."""
266        if value.lower() in {"true", "1", "yes"}:
267            return True
268        if value.lower() in {"false", "0", "no"}:
269            return False
270        msg = f"Cannot convert {value} to boolean."
271        raise ValueError(msg)

A config value that is a boolean.

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

Initialize the base data type.

def convert(self, value: str) -> bool:
264    def convert(self, value: str) -> bool:
265        """Convert a string value to a boolean."""
266        if value.lower() in {"true", "1", "yes"}:
267            return True
268        if value.lower() in {"false", "0", "no"}:
269            return False
270        msg = f"Cannot convert {value} to boolean."
271        raise ValueError(msg)

Convert a string value to a boolean.

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

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
 95        if not self.optional and default is UNSET:
 96            msg = "Default value cannot be None when optional is False."
 97            raise InvalidDefaultError(msg)
 98
 99        if not self._parser:
100            self._detect_parser()
101
102        self._initialize_data_type(default)
103        self._validate_init()
104        self._read_parser()

Initialize the config descriptor with a default value.

Validate that parser and filepath are present.

validate_types: ClassVar[bool] = True

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

write_on_edit: ClassVar[bool] = True

Write to the config file when updating a value.

optional: bool = False

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

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

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

def validate_strict_type(self) -> None:
157    def validate_strict_type(self) -> None:
158        """Validate the type of the converter matches the desired type."""
159        if self._data_type.convert is UNSET:
160            msg = "Converter is not set."
161            raise InvalidConverterError(msg)
162
163        cls = self.__class__
164        self.__config_value = cls._parser.get(self._section, self._setting)
165        self.__converted_value = self.convert(self.__config_value)
166
167        if not cls.validate_types:
168            return
169
170        self.__converted_type = type(self.__converted_value)
171        default_value_type = type(self._data_type.default)
172
173        is_optional = self.optional or isinstance(self._data_type, Optional)
174        if (is_optional) and self.__converted_type in (default_value_type, NoneType):
175            # Allow None or the same type as the default value to be returned by the converter when _optional is True.
176            return
177        if self.__converted_type is not default_value_type:
178            msg = f"Converter does not return the same type as the default value <{default_value_type}> got <{self.__converted_type}>."  # noqa: E501
179            raise InvalidConverterError(msg)
180
181        # Set the data_type value. ensuring validation works as expected.
182        self._data_type.value = self.__converted_value
183        if not self._data_type.validate():
184            msg = f"Invalid value for {self._section}.{self._setting}: {self.__converted_value}"
185            raise InvalidConverterError(msg)

Validate the type of the converter matches the desired type.

def on_file_change(self, origin: Literal['get', 'set'], old: ~VT, new: ~VT) -> None:
187    def on_file_change(self, origin: Literal["get", "set"], old: VT, new: VT) -> None:
188        """Triggered when the config file changes.
189
190        This needs to be implemented by a subclass before it's usable.
191        This will be called **before** setting the value from the config file.
192        This will be called **after** getting (but before validating it's type) the value from config file.
193        The `origin` parameter indicates whether the change was triggered by a `get` or `set` operation.
194        """

Triggered when the config file changes.

This needs to be implemented by a subclass 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.

@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:
196    @classmethod
197    @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
198    def set_parser(cls, parser: ConfkitParser) -> None:
199        """Set the parser for ALL descriptor instances (of this type/class)."""
200        if cls is Config:
201            cls._warn_base_class_usage()
202        cls._parser = parser

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

@classmethod
def set_file(cls, file: pathlib.Path) -> None:
204    @classmethod
205    def set_file(cls, file: Path) -> None:
206        """Set the file for ALL descriptors."""
207        if cls is Config:
208            cls._warn_base_class_usage()
209        cls._file = file
210        cls._watcher = FileWatcher(file)

Set the file for ALL descriptors.

@classmethod
def validate_file(cls) -> None:
212    @classmethod
213    def validate_file(cls) -> None:
214        """Validate the config file."""
215        if cls._file is UNSET:
216            msg = f"Config file is not set. use {cls.__name__}.set_file() to set it."
217            raise ValueError(msg)

Validate the config file.

@classmethod
def validate_parser(cls) -> None:
219    @classmethod
220    def validate_parser(cls) -> None:
221        """Validate the config parser."""
222        if cls._parser is UNSET:
223            msg = f"Config parser is not set. use {cls.__name__}.set_parser() to set it."
224            raise ValueError(msg)

Validate the config parser.

@classmethod
def write(cls) -> None:
227    @classmethod
228    def write(cls) -> None:
229        """Write the config parser to the file."""
230        cls.validate_file()
231        with cls._file.open("w") as f:
232            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]]:
234    @classmethod
235    def set(cls, section: str, setting: str, value: VT) -> Callable[[Callable[P, F]], Callable[P, F]]:
236        """Set a config value using this descriptor."""
237
238        def wrapper(func: Callable[P, F]) -> Callable[P, F]:
239            @wraps(func)
240            def inner(*args: P.args, **kwargs: P.kwargs) -> F:
241                cls._set(section, setting, value)
242                return func(*args, **kwargs)
243
244            return inner
245        return wrapper

Set a config value using this descriptor.

@classmethod
def with_setting( cls, setting: Config[~OVT]) -> Callable[[Callable[~P, ~F]], Callable[~P, ~F]]:
248    @classmethod
249    def with_setting(cls, setting: Config[OVT]) -> Callable[[Callable[P, F]], Callable[P, F]]:
250        """Insert a config value into **kwargs to the wrapped method/function using this decorator."""
251        def wrapper(func: Callable[P, F]) -> Callable[P, F]:
252            @wraps(func)
253            def inner(*args: P.args, **kwargs: P.kwargs) -> F:
254                kwargs[setting.name] = setting.convert(cls._parser.get(setting._section, setting._setting))
255                return func(*args, **kwargs)
256
257            return inner
258        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]]:
261    @classmethod
262    def with_kwarg(
263        cls, section: str, setting: str, name: str | None = None, default: VT = UNSET,
264    ) -> Callable[[Callable[P, F]], Callable[P, F]]:
265        """Insert a config value into **kwargs to the wrapped method/function using this descriptor.
266
267        Use kwarg.get(`name`) to get the value.
268        `name` is the name the kwarg gets if passed, if None, it will be the same as `setting`.
269        Section parameter is just for finding the config value.
270        """
271        if name is None:
272            name = setting
273        if default is UNSET and not cls._parser.has_option(section, setting):
274            msg = f"Config value {section=} {setting=} is not set. and no default value is given."
275            raise ValueError(msg)
276
277        def wrapper(func: Callable[P, F]) -> Callable[P, F]:
278            @wraps(func)
279            def inner(*args: P.args, **kwargs: P.kwargs) -> F:
280                if default is not UNSET:
281                    cls._set_default(section, setting, default)
282                kwargs[name] = cls._parser.get(section, setting)
283                return func(*args, **kwargs)
284
285            return inner
286        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]]:
288    @classmethod
289    def default(cls, section: str, setting: str, value: VT) -> Callable[[Callable[P, F]], Callable[P, F]]:
290        """Set a default config value if none are set yet using this descriptor."""
291        def wrapper(func: Callable[P, F]) -> Callable[P, F]:
292            @wraps(func)
293            def inner(*args: P.args, **kwargs: P.kwargs) -> F:
294                cls._set_default(section, setting, value)
295                return func(*args, **kwargs)
296
297            return inner
298        return wrapper

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

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

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

class ConfigPathConflictError(builtins.ValueError):
14class ConfigPathConflictError(ValueError):
15    """Raised when a configuration path conflicts with an existing scalar value.
16
17    This occurs when attempting to treat a scalar value as a section (dict).
18    For example, if "Parent.Value" is a scalar, attempting to set "Parent.Value.Child"
19    would cause this error.
20    """

Raised when a configuration path conflicts with an existing scalar value.

This occurs when attempting to treat a scalar value as a section (dict). For example, if "Parent.Value" is a scalar, attempting to set "Parent.Value.Child" would cause this error.

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

A config value that is a date.

Date(default: 'date | UNSET' = MISSING, **kwargs: Any)
683    def __init__(self, default: date | UNSET = UNSET, **kwargs: Any) -> None:
684        """Initialize the date data type. Defaults to current date if not provided."""
685        if default is UNSET:
686            default = date(**kwargs)
687        super().__init__(default)

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

@override
def convert(self, value: str) -> datetime.date:
693    @override
694    def convert(self, value: str) -> date:
695        """Convert a string value to a date."""
696        return date.fromisoformat(value)

Convert a string value to a date.

class DateTime(confkit.BaseDataType[datetime.datetime]):
643class DateTime(BaseDataType[datetime]):
644    """A config value that is a datetime."""
645
646    @overload
647    def __init__(self, default: datetime = UNSET) -> None: ...
648    @overload
649    def __init__(self, **kwargs: Unpack[_DateTimeKwargs]) -> None: ...
650
651    def __init__(self, default: datetime | UNSET = UNSET, **kwargs: Any) -> None:
652        """Initialize the datetime data type."""
653        if default is UNSET:
654            try:
655                default = datetime(**kwargs)  # noqa: DTZ001 Tzinfo is (optionally) passed using kwargs
656            except TypeError:
657                default = datetime.now(tz=UTC)
658        super().__init__(default)
659
660    @override
661    def __str__(self) -> str:
662        """Return the string representation of the stored value."""
663        return self.value.isoformat()
664
665    @override
666    def convert(self, value: str) -> datetime:
667        """Convert a string value to a datetime."""
668        return datetime.fromisoformat(value)

A config value that is a datetime.

DateTime(default: 'datetime | UNSET' = MISSING, **kwargs: Any)
651    def __init__(self, default: datetime | UNSET = UNSET, **kwargs: Any) -> None:
652        """Initialize the datetime data type."""
653        if default is UNSET:
654            try:
655                default = datetime(**kwargs)  # noqa: DTZ001 Tzinfo is (optionally) passed using kwargs
656            except TypeError:
657                default = datetime.now(tz=UTC)
658        super().__init__(default)

Initialize the datetime data type.

@override
def convert(self, value: str) -> datetime.datetime:
665    @override
666    def convert(self, value: str) -> datetime:
667        """Convert a string value to a datetime."""
668        return datetime.fromisoformat(value)

Convert a string value to a datetime.

class Dict(confkit.BaseDataType[dict[~KT, ~VT]], typing.Generic[~KT, ~VT]):
549class Dict(BaseDataType[dict[KT, VT]], Generic[KT, VT]):
550    """A config value that is a dictionary of string keys and values of type T."""
551
552    @overload
553    def __init__(self, default: dict[KT, VT]) -> None: ...
554    @overload
555    def __init__(self, *, key_type: BaseDataType[KT], value_type: BaseDataType[VT]) -> None: ...
556    @overload
557    def __init__(
558        self,
559        default: dict[KT, VT],
560        *,
561        key_type: BaseDataType[KT] = ...,
562        value_type: BaseDataType[VT] = ...,
563    ) -> None: ...
564
565    def __init__(
566        self,
567        default: dict[KT, VT] = UNSET,
568        *,
569        key_type: BaseDataType[KT] = UNSET,
570        value_type: BaseDataType[VT] = UNSET,
571    ) -> None:
572        """Initialize the dict data type."""
573        if default is UNSET and (key_type is UNSET or value_type is UNSET):
574            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
575            raise InvalidDefaultError(msg)
576        if default is UNSET:
577            default = {}
578        super().__init__(default)
579
580        self._infer_key_type(default, key_type)
581        self._infer_value_type(default, value_type)
582
583    def __str__(self) -> str:
584        """Return a string representation of the dictionary."""
585        items = [
586            f"{self._key_data_type.convert(str(k))}={self._value_data_type.convert(str(v))}"
587            for k, v in self.value.items()
588        ]
589        return ",".join(items)
590
591    def convert(self, value: str) -> dict[KT, VT]:
592        """Convert a string to a dictionary."""
593        if not value:
594            return {}
595
596        parts = value.split(",")
597        result: dict[KT, VT] = {}
598        for part in parts:
599            if "=" not in part:
600                msg = f"Invalid dictionary entry: {part}. Expected format key=value."
601                raise ValueError(msg)
602            key_str, val_str = part.split("=", 1)
603            key = self._key_data_type.convert(key_str.strip())
604            val = self._value_data_type.convert(val_str.strip())
605            result[key] = val
606        return result
607
608    def _infer_key_type(self, default: dict[KT, VT], key_type: BaseDataType[KT]) -> None:
609        """Infer the key type from the default dictionary if not provided."""
610        if len(default.keys()) <= 0 and key_type is UNSET:
611            msg = "Dict default must have at least one key element to infer type. or specify `key_type=<BaseDataType>`"
612            raise InvalidDefaultError(msg)
613        if key_type is UNSET:
614            for key in default:
615                self._key_data_type = BaseDataType[KT].cast(key)
616                break
617        else:
618            self._key_data_type = key_type
619
620    def _infer_value_type(self, default: dict[KT, VT], value_type: BaseDataType[VT]) -> None:
621        """Infer the value type from the default dictionary if not provided."""
622        if len(default.values()) <= 0 and value_type is UNSET:
623            msg = "Dict default must have at least one value element to infer type. or specify `value_type=<BaseDataType>`"
624            raise InvalidDefaultError(msg)
625        if value_type is UNSET:
626            for value in default.values():
627                self._value_data_type = BaseDataType[VT].cast(value)
628                break
629        else:
630            self._value_data_type = value_type

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)
565    def __init__(
566        self,
567        default: dict[KT, VT] = UNSET,
568        *,
569        key_type: BaseDataType[KT] = UNSET,
570        value_type: BaseDataType[VT] = UNSET,
571    ) -> None:
572        """Initialize the dict data type."""
573        if default is UNSET and (key_type is UNSET or value_type is UNSET):
574            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
575            raise InvalidDefaultError(msg)
576        if default is UNSET:
577            default = {}
578        super().__init__(default)
579
580        self._infer_key_type(default, key_type)
581        self._infer_value_type(default, value_type)

Initialize the dict data type.

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

Convert a string to a dictionary.

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

A config value that is an enum.

def convert(self, value: str) -> ~EnumType:
146    def convert(self, value: str) -> EnumType:
147        """Convert a string value to an enum."""
148        value = self._strip_comment(value)
149        parsed_enum_name = value.split(".")[-1]
150        return self.value.__class__[parsed_enum_name]

Convert a string value to an enum.

class Float(confkit.BaseDataType[float]):
247class Float(BaseDataType[float]):
248    """A config value that is a float."""
249
250    def __init__(self, default: float = 0.0) -> None:  # noqa: D107
251        super().__init__(default)
252
253    def convert(self, value: str) -> float:
254        """Convert a string value to a float."""
255        return float(value)

A config value that is a float.

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

Initialize the base data type.

def convert(self, value: str) -> float:
253    def convert(self, value: str) -> float:
254        """Convert a string value to a float."""
255        return float(value)

Convert a string value to a float.

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

A config value that represents hexadecimal.

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

Initialize the base data type.

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

Convert a string value to an integer. from hexadecimal.

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

A config value that is an enum.

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

Convert a string value to an enum.

class IntFlag(confkit.data_types._EnumBase[~IntFlagType]):
198class IntFlag(_EnumBase[IntFlagType]):
199    """A config value that is an enum."""
200
201    def convert(self, value: str) -> IntFlagType:
202        """Convert a string value to an enum."""
203        value = self._strip_comment(value)
204        return self.value.__class__(int(value))
205
206    def _format_allowed_values(self) -> str:
207        """Format allowed values as comma-separated name(value) pairs."""
208        enum_class = self.value.__class__
209        return ", ".join(f"{member.name}({member.value})" for member in enum_class)
210
211    def _get_value_str(self) -> str:
212        """Get the member value as string."""
213        return str(self.value.value)

A config value that is an enum.

def convert(self, value: str) -> ~IntFlagType:
201    def convert(self, value: str) -> IntFlagType:
202        """Convert a string value to an enum."""
203        value = self._strip_comment(value)
204        return self.value.__class__(int(value))

Convert a string value to an enum.

class Integer(confkit.BaseDataType[int]):
278class Integer(BaseDataType[int]):
279    """A config value that is an integer."""
280
281    # Define constants for common bases
282
283    def __init__(self, default: int = 0, base: int = DECIMAL) -> None:  # noqa: D107
284        super().__init__(default)
285        self.base = base
286
287    def __str__(self) -> str:  # noqa: D105
288        if self.base == DECIMAL:
289            return str(self.value)
290        # Convert the base 10 int to base 5
291        self.value = self.int_to_base(int(self.value), self.base)
292        return f"{self.base}c{self.value}"
293
294    def convert(self, value: str) -> int:
295        """Convert a string value to an integer."""
296        if "c" in value:
297            base_str, val_str = value.split("c")
298            base = int(base_str)
299            if base != self.base:
300                msg = "Base in string does not match base in Integer while converting."
301                raise ValueError(msg)
302            return int(val_str, self.base)
303        return int(value, self.base)
304
305    @staticmethod
306    def int_to_base(number: int, base: int) -> int:
307        """Convert an integer to a string representation in a given base."""
308        if number == 0:
309            return 0
310        digits = []
311        while number:
312            digits.append(str(number % base))
313            number //= base
314        return int("".join(reversed(digits)))

A config value that is an integer.

Integer(default: int = 0, base: int = 10)
283    def __init__(self, default: int = 0, base: int = DECIMAL) -> None:  # noqa: D107
284        super().__init__(default)
285        self.base = base

Initialize the base data type.

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

Convert a string value to an integer.

@staticmethod
def int_to_base(number: int, base: int) -> int:
305    @staticmethod
306    def int_to_base(number: int, base: int) -> int:
307        """Convert an integer to a string representation in a given base."""
308        if number == 0:
309            return 0
310        digits = []
311        while number:
312            digits.append(str(number % base))
313            number //= base
314        return int("".join(reversed(digits)))

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

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

Raised when the converter is not set or invalid.

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

Raised when the default value is not set or invalid.

class List(confkit.data_types._SequenceType[~T], typing.Generic[~T]):
486class List(_SequenceType[T], Generic[T]):
487    """A config value that is a list of values."""
488
489    def convert(self, value: str) -> list[T]:
490        """Convert a string to a list."""
491        return list(super()._convert(value))

A config value that is a list of values.

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

Convert a string to a list.

class NoneType(confkit.BaseDataType[NoneType]):
215class NoneType(BaseDataType[None]):
216    """A config value that is None."""
217
218    null_values: ClassVar[set[str]] = {"none", "null", "nil"}
219
220    def __init__(self) -> None:
221        """Initialize the NoneType data type."""
222        super().__init__(None)
223
224    def is_valid(self, value: str) -> bool:
225        """Check if the provided string value is in the set of null values."""
226        return value.casefold().strip() in NoneType.null_values
227
228    def convert(self, value: str) -> None:
229        """Convert a string value to None."""
230        if self.is_valid(value):
231            return
232        msg = f"Value '{value}' is not a valid null value. Expected one of: {', '.join(NoneType.null_values)}."
233        raise ValueError(msg)

A config value that is None.

NoneType()
220    def __init__(self) -> None:
221        """Initialize the NoneType data type."""
222        super().__init__(None)

Initialize the NoneType data type.

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

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

def convert(self, value: str) -> None:
228    def convert(self, value: str) -> None:
229        """Convert a string value to None."""
230        if self.is_valid(value):
231            return
232        msg = f"Value '{value}' is not a valid null value. Expected one of: {', '.join(NoneType.null_values)}."
233        raise ValueError(msg)

Convert a string value to None.

class Octal(confkit.BaseDataType[int]):
329class Octal(Integer):
330    """A config value that represents octal."""
331
332    def __init__(self, default: int = 0, base: int = OCTAL) -> None:  # noqa: D107
333        super().__init__(default, base)
334
335    def __str__(self) -> str:  # noqa: D105
336        return f"0o{self.value:o}"
337
338    def convert(self, value: str) -> int:
339        """Convert a string value to an integer from octal."""
340        return int(value.removeprefix("0o"), 8)

A config value that represents octal.

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

Initialize the base data type.

def convert(self, value: str) -> int:
338    def convert(self, value: str) -> int:
339        """Convert a string value to an integer from octal."""
340        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]):
362class Optional(BaseDataType[T | None], Generic[T]):
363    """A config value that is optional, can be None or a specific type."""
364
365    _none_type = NoneType()
366
367    def __init__(self, data_type: BaseDataType[T]) -> None:
368        """Initialize the optional data type. Wrapping the provided data type."""
369        self._data_type = data_type
370
371    def __str__(self) -> str:
372        """Return the string representation of the wrapped data type."""
373        return str(self._data_type)
374
375    @property
376    def default(self) -> T | None:
377        """Get the default value of the wrapped data type."""
378        return self._data_type.default
379
380    @property
381    def value(self) -> T | None:
382        """Get the current value of the wrapped data type."""
383        return self._data_type.value
384
385    @value.setter
386    def value(self, value: T | None) -> None:
387        """Set the current value of the wrapped data type."""
388        self._data_type.value = value
389
390    def convert(self, value: str) -> T | None:
391        """Convert a string value to the optional type."""
392        if self._none_type.is_valid(value):
393            return self._none_type.convert(value)
394        return self._data_type.convert(value)
395
396    def validate(self) -> bool:
397        """Validate that the value is of the wrapped data type or None."""
398        if self._data_type.value is None:
399            return True
400        return self._data_type.validate()

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

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

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

default: Optional[~T]
375    @property
376    def default(self) -> T | None:
377        """Get the default value of the wrapped data type."""
378        return self._data_type.default

Get the default value of the wrapped data type.

value: Optional[~T]
380    @property
381    def value(self) -> T | None:
382        """Get the current value of the wrapped data type."""
383        return self._data_type.value

Get the current value of the wrapped data type.

def convert(self, value: str) -> Optional[~T]:
390    def convert(self, value: str) -> T | None:
391        """Convert a string value to the optional type."""
392        if self._none_type.is_valid(value):
393            return self._none_type.convert(value)
394        return self._data_type.convert(value)

Convert a string value to the optional type.

def validate(self) -> bool:
396    def validate(self) -> bool:
397        """Validate that the value is of the wrapped data type or None."""
398        if self._data_type.value is None:
399            return True
400        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]):
500class Set(BaseDataType[set[T]], Generic[T]):
501    """A config value that is a set of values."""
502
503    @overload
504    def __init__(self, default: set[T]) -> None: ...
505    @overload
506    def __init__(self, *, data_type: BaseDataType[T]) -> None: ...
507    @overload
508    def __init__(
509        self,
510        default: set[T],
511        *,
512        data_type: BaseDataType[T] = ...,
513    ) -> None: ...
514
515    def __init__(self, default: set[T] = UNSET, *, data_type: BaseDataType[T] = UNSET) -> None:
516        """Initialize the set data type."""
517        if default is UNSET and data_type is UNSET:
518            msg = "Set requires either a default with at least one element, or data_type to be specified."
519            raise InvalidDefaultError(msg)
520        if default is UNSET:
521            default = set()
522        super().__init__(default)
523        self._infer_type(default, data_type)
524
525    def __str__(self) -> str:
526        """Return a string representation of the set."""
527        return ",".join(str(item) for item in self.value)
528
529    def convert(self, value: str) -> set[T]:
530        """Convert a string to a set."""
531        if not value:
532            return set()
533        parts = value.split(",")
534        return {self._data_type.convert(item.strip()) for item in parts}
535
536    def _infer_type(self, default: set[T], data_type: BaseDataType[T]) -> None:
537        if len(default) <= 0 and data_type is UNSET:
538            msg = "Set default must have at least one element to infer type. or specify `data_type=<BaseDataType>`"
539            raise InvalidDefaultError(msg)
540        if data_type is UNSET:
541            sample_element = default.pop()
542            default.add(sample_element)
543            self._data_type = BaseDataType[T].cast(sample_element)
544        else:
545            self._data_type = data_type

A config value that is a set of values.

Set( default: set[~T] = MISSING, *, data_type: BaseDataType[~T] = MISSING)
515    def __init__(self, default: set[T] = UNSET, *, data_type: BaseDataType[T] = UNSET) -> None:
516        """Initialize the set data type."""
517        if default is UNSET and data_type is UNSET:
518            msg = "Set requires either a default with at least one element, or data_type to be specified."
519            raise InvalidDefaultError(msg)
520        if default is UNSET:
521            default = set()
522        super().__init__(default)
523        self._infer_type(default, data_type)

Initialize the set data type.

def convert(self, value: str) -> set[~T]:
529    def convert(self, value: str) -> set[T]:
530        """Convert a string to a set."""
531        if not value:
532            return set()
533        parts = value.split(",")
534        return {self._data_type.convert(item.strip()) for item in parts}

Convert a string to a set.

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

A config value that is an enum.

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

Convert a string value to an enum.

class String(confkit.BaseDataType[str]):
236class String(BaseDataType[str]):
237    """A config value that is a string."""
238
239    def __init__(self, default: str = "") -> None:  # noqa: D107
240        super().__init__(default)
241
242    def convert(self, value: str) -> str:
243        """Convert a string value to a string."""
244        return value

A config value that is a string.

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

Initialize the base data type.

def convert(self, value: str) -> str:
242    def convert(self, value: str) -> str:
243        """Convert a string value to a string."""
244        return value

Convert a string value to a string.

class Time(confkit.BaseDataType[datetime.time]):
706class Time(BaseDataType[time]):
707    """A config value that is a time."""
708
709    @overload
710    def __init__(self, default: time = UNSET) -> None: ...
711    @overload
712    def __init__(self, **kwargs: Unpack[_TimeKwargs]) -> None: ...
713
714    def __init__(self, default: time | UNSET = UNSET, **kwargs: Unpack[_TimeKwargs]) -> None:
715        """Initialize the time data type. Defaults to current time if not provided."""
716        if default is UNSET:
717            default = time(**kwargs)
718        super().__init__(default)
719
720    @override
721    def __str__(self) -> str:
722        return self.value.isoformat()
723
724    @override
725    def convert(self, value: str) -> time:
726        """Convert a string value to a time."""
727        return time.fromisoformat(value)

A config value that is a time.

Time( default: 'time | UNSET' = MISSING, **kwargs: *<class 'confkit.data_types._TimeKwargs'>)
714    def __init__(self, default: time | UNSET = UNSET, **kwargs: Unpack[_TimeKwargs]) -> None:
715        """Initialize the time data type. Defaults to current time if not provided."""
716        if default is UNSET:
717            default = time(**kwargs)
718        super().__init__(default)

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

@override
def convert(self, value: str) -> datetime.time:
724    @override
725    def convert(self, value: str) -> time:
726        """Convert a string value to a time."""
727        return time.fromisoformat(value)

Convert a string value to a time.

class TimeDelta(confkit.BaseDataType[datetime.timedelta]):
738class TimeDelta(BaseDataType[timedelta]):
739    """A config value that is a timedelta."""
740
741    def __init__(
742        self,
743        default: timedelta = UNSET,
744        **kwargs: Unpack[_TimeDeltaKwargs],
745    ) -> None:
746        """Initialize the timedelta data type. Defaults to 0 if not provided."""
747        if default is UNSET:
748            default = timedelta(**kwargs)
749        super().__init__(default)
750
751    def __str__(self) -> str:  # noqa: D105
752        return str(self.value.total_seconds())
753
754    def convert(self, value: str) -> timedelta:
755        """Convert a string value to a timedelta."""
756        return timedelta(seconds=float(value))

A config value that is a timedelta.

TimeDelta( default: datetime.timedelta = MISSING, **kwargs: *<class 'confkit.data_types._TimeDeltaKwargs'>)
741    def __init__(
742        self,
743        default: timedelta = UNSET,
744        **kwargs: Unpack[_TimeDeltaKwargs],
745    ) -> None:
746        """Initialize the timedelta data type. Defaults to 0 if not provided."""
747        if default is UNSET:
748            default = timedelta(**kwargs)
749        super().__init__(default)

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

def convert(self, value: str) -> datetime.timedelta:
754    def convert(self, value: str) -> timedelta:
755        """Convert a string value to a timedelta."""
756        return timedelta(seconds=float(value))

Convert a string value to a timedelta.

class Tuple(confkit.data_types._SequenceType[~T], typing.Generic[~T]):
493class Tuple(_SequenceType[T], Generic[T]):
494    """A config value that is a tuple of values."""
495
496    def convert(self, value: str) -> tuple[T, ...]:
497        """Convert a string to a tuple."""
498        return tuple(super()._convert(value))

A config value that is a tuple of values.

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

Convert a string to a tuple.