confkit 1.3.0__tar.gz → 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. {confkit-1.3.0 → confkit-2.0.0}/.github/workflows/bump-version-by-labels.yml +2 -1
  2. confkit-2.0.0/.github/workflows/lint.yml +40 -0
  3. {confkit-1.3.0 → confkit-2.0.0}/.github/workflows/test.yml +2 -9
  4. {confkit-1.3.0 → confkit-2.0.0}/PKG-INFO +1 -1
  5. {confkit-1.3.0 → confkit-2.0.0}/pyproject.toml +1 -1
  6. {confkit-1.3.0 → confkit-2.0.0}/ruff.toml +8 -2
  7. {confkit-1.3.0 → confkit-2.0.0}/src/confkit/__init__.py +1 -0
  8. {confkit-1.3.0 → confkit-2.0.0}/src/confkit/config.py +6 -7
  9. {confkit-1.3.0 → confkit-2.0.0}/src/confkit/data_types.py +27 -20
  10. {confkit-1.3.0 → confkit-2.0.0}/src/confkit/exceptions.py +2 -0
  11. {confkit-1.3.0 → confkit-2.0.0}/src/confkit/ext/__init__.py +1 -0
  12. {confkit-1.3.0 → confkit-2.0.0}/src/confkit/ext/pydantic.py +7 -1
  13. {confkit-1.3.0 → confkit-2.0.0}/src/confkit/ext/reference.py +1 -0
  14. {confkit-1.3.0 → confkit-2.0.0}/src/confkit/parsers.py +0 -2
  15. {confkit-1.3.0 → confkit-2.0.0}/src/confkit/sentinels.py +1 -0
  16. {confkit-1.3.0 → confkit-2.0.0}/src/confkit/watcher.py +6 -1
  17. {confkit-1.3.0 → confkit-2.0.0}/tests/test_config.py +10 -8
  18. {confkit-1.3.0 → confkit-2.0.0}/tests/test_config_classvars.py +8 -4
  19. {confkit-1.3.0 → confkit-2.0.0}/tests/test_config_decorators.py +7 -3
  20. {confkit-1.3.0 → confkit-2.0.0}/tests/test_config_detect_parser.py +2 -0
  21. {confkit-1.3.0 → confkit-2.0.0}/tests/test_data_types.py +2 -0
  22. {confkit-1.3.0 → confkit-2.0.0}/tests/test_enum_allowed_values.py +2 -0
  23. confkit-2.0.0/tests/test_enum_auto_conversion.py +146 -0
  24. {confkit-1.3.0 → confkit-2.0.0}/tests/test_list_type.py +1 -0
  25. {confkit-1.3.0 → confkit-2.0.0}/tests/test_metaclass.py +2 -0
  26. {confkit-1.3.0 → confkit-2.0.0}/tests/test_msgspec.py +2 -0
  27. {confkit-1.3.0 → confkit-2.0.0}/tests/test_msgspecparser_branches.py +2 -0
  28. {confkit-1.3.0 → confkit-2.0.0}/tests/test_msgspecparser_no_msgspec.py +2 -0
  29. {confkit-1.3.0 → confkit-2.0.0}/tests/test_msgspecparser_read.py +6 -1
  30. {confkit-1.3.0 → confkit-2.0.0}/tests/test_msgspecparser_sections.py +2 -0
  31. {confkit-1.3.0 → confkit-2.0.0}/tests/test_multiple_configurations.py +2 -0
  32. {confkit-1.3.0 → confkit-2.0.0}/tests/test_on_file_change.py +2 -0
  33. {confkit-1.3.0 → confkit-2.0.0}/tests/test_sentinel.py +1 -0
  34. {confkit-1.3.0 → confkit-2.0.0}/tests/test_two_instances.py +2 -0
  35. {confkit-1.3.0 → confkit-2.0.0}/tests/test_unreachable_paths.py +2 -1
  36. {confkit-1.3.0 → confkit-2.0.0}/.bumpversion.toml +0 -0
  37. {confkit-1.3.0 → confkit-2.0.0}/.github/FUNDING.yml +0 -0
  38. {confkit-1.3.0 → confkit-2.0.0}/.github/copilot-instructions.md +0 -0
  39. {confkit-1.3.0 → confkit-2.0.0}/.github/dependabot.yml +0 -0
  40. {confkit-1.3.0 → confkit-2.0.0}/.github/workflows/coverage.yml +0 -0
  41. {confkit-1.3.0 → confkit-2.0.0}/.github/workflows/docs.yml +0 -0
  42. {confkit-1.3.0 → confkit-2.0.0}/.github/workflows/pypi.yml +0 -0
  43. {confkit-1.3.0 → confkit-2.0.0}/.github/workflows/release.yml +0 -0
  44. {confkit-1.3.0 → confkit-2.0.0}/.gitignore +0 -0
  45. {confkit-1.3.0 → confkit-2.0.0}/.pre-commit-config.yaml +0 -0
  46. {confkit-1.3.0 → confkit-2.0.0}/.python-version +0 -0
  47. {confkit-1.3.0 → confkit-2.0.0}/.vscode/settings.json +0 -0
  48. {confkit-1.3.0 → confkit-2.0.0}/README.md +0 -0
  49. {confkit-1.3.0 → confkit-2.0.0}/docs/examples/argparse.md +0 -0
  50. {confkit-1.3.0 → confkit-2.0.0}/docs/examples/basic.md +0 -0
  51. {confkit-1.3.0 → confkit-2.0.0}/docs/examples/custom_data_type.md +0 -0
  52. {confkit-1.3.0 → confkit-2.0.0}/docs/examples/data_types.md +0 -0
  53. {confkit-1.3.0 → confkit-2.0.0}/docs/examples/decorators.md +0 -0
  54. {confkit-1.3.0 → confkit-2.0.0}/docs/examples/enums.md +0 -0
  55. {confkit-1.3.0 → confkit-2.0.0}/docs/examples/index.md +0 -0
  56. {confkit-1.3.0 → confkit-2.0.0}/docs/examples/list_types.md +0 -0
  57. {confkit-1.3.0 → confkit-2.0.0}/docs/examples/optional_values.md +0 -0
  58. {confkit-1.3.0 → confkit-2.0.0}/docs/index.md +0 -0
  59. {confkit-1.3.0 → confkit-2.0.0}/docs/reference/config.md +0 -0
  60. {confkit-1.3.0 → confkit-2.0.0}/docs/reference/data_types.md +0 -0
  61. {confkit-1.3.0 → confkit-2.0.0}/docs/reference/exceptions.md +0 -0
  62. {confkit-1.3.0 → confkit-2.0.0}/docs/reference/notes_two_instances.md +0 -0
  63. {confkit-1.3.0 → confkit-2.0.0}/docs/usage.md +0 -0
  64. {confkit-1.3.0 → confkit-2.0.0}/examples/api.ini +0 -0
  65. {confkit-1.3.0 → confkit-2.0.0}/examples/argparse_example.py +0 -0
  66. {confkit-1.3.0 → confkit-2.0.0}/examples/args.ini +0 -0
  67. {confkit-1.3.0 → confkit-2.0.0}/examples/basic.py +0 -0
  68. {confkit-1.3.0 → confkit-2.0.0}/examples/config.ini +0 -0
  69. {confkit-1.3.0 → confkit-2.0.0}/examples/custom_data_type.py +0 -0
  70. {confkit-1.3.0 → confkit-2.0.0}/examples/data_types.py +0 -0
  71. {confkit-1.3.0 → confkit-2.0.0}/examples/database.ini +0 -0
  72. {confkit-1.3.0 → confkit-2.0.0}/examples/decorators.py +0 -0
  73. {confkit-1.3.0 → confkit-2.0.0}/examples/enums.py +0 -0
  74. {confkit-1.3.0 → confkit-2.0.0}/examples/example.json +0 -0
  75. {confkit-1.3.0 → confkit-2.0.0}/examples/example.toml +0 -0
  76. {confkit-1.3.0 → confkit-2.0.0}/examples/example.yaml +0 -0
  77. {confkit-1.3.0 → confkit-2.0.0}/examples/file_change_event.py +0 -0
  78. {confkit-1.3.0 → confkit-2.0.0}/examples/list_types.py +0 -0
  79. {confkit-1.3.0 → confkit-2.0.0}/examples/multiple_configs.py +0 -0
  80. {confkit-1.3.0 → confkit-2.0.0}/examples/nested_config.py +0 -0
  81. {confkit-1.3.0 → confkit-2.0.0}/examples/nested_example.ini +0 -0
  82. {confkit-1.3.0 → confkit-2.0.0}/examples/nested_example.json +0 -0
  83. {confkit-1.3.0 → confkit-2.0.0}/examples/nested_example.toml +0 -0
  84. {confkit-1.3.0 → confkit-2.0.0}/examples/nested_example.yaml +0 -0
  85. {confkit-1.3.0 → confkit-2.0.0}/examples/optional_values.py +0 -0
  86. {confkit-1.3.0 → confkit-2.0.0}/examples/other_file_types.py +0 -0
  87. {confkit-1.3.0 → confkit-2.0.0}/examples/pydantic_example.py +0 -0
  88. {confkit-1.3.0 → confkit-2.0.0}/examples/references.py +0 -0
  89. {confkit-1.3.0 → confkit-2.0.0}/examples/url_example.py +0 -0
  90. {confkit-1.3.0 → confkit-2.0.0}/mkdocs.yml +0 -0
  91. {confkit-1.3.0 → confkit-2.0.0}/src/confkit/ext/parsers.py +0 -0
  92. {confkit-1.3.0 → confkit-2.0.0}/src/confkit/py.typed +0 -0
  93. {confkit-1.3.0 → confkit-2.0.0}/tests/__init__.py +0 -0
  94. {confkit-1.3.0 → confkit-2.0.0}/tests/conftest.py +0 -0
  95. {confkit-1.3.0 → confkit-2.0.0}/tests/test_env_parser.py +0 -0
  96. {confkit-1.3.0 → confkit-2.0.0}/tests/test_examples_run.py +0 -0
  97. {confkit-1.3.0 → confkit-2.0.0}/tests/test_nested_config.py +0 -0
  98. {confkit-1.3.0 → confkit-2.0.0}/tests/test_pydantic_models.py +0 -0
@@ -30,9 +30,10 @@ jobs:
30
30
 
31
31
  - name: Determine version bump
32
32
  id: detect-bump
33
+ env:
34
+ LABELS: ${{ github.event.pull_request.labels.*.name }}
33
35
  run: |
34
36
  # Check PR labels to determine bump type
35
- LABELS="${{ github.event.pull_request.labels.*.name }}"
36
37
  if echo "$LABELS" | grep -q "Major"; then
37
38
  echo "bump_type=major" >> $env:GITHUB_OUTPUT
38
39
  elif echo "$LABELS" | grep -q "Minor"; then
@@ -0,0 +1,40 @@
1
+ name: Lint & Type Check
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ branches: [master]
8
+ workflow_dispatch:
9
+ workflow_call:
10
+
11
+ concurrency: lint-${{ github.sha }}
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ lint:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Cache lint results
23
+ id: cache-lint
24
+ uses: actions/cache@v4
25
+ with:
26
+ path: .lint_cache
27
+ key: ${{ runner.os }}-lint-${{ hashFiles('**/tests/**/*.py', 'src/**') }}
28
+ restore-keys: |
29
+ ${{ runner.os }}-lint
30
+
31
+ - name: Install uv
32
+ uses: astral-sh/setup-uv@v7
33
+ with:
34
+ enable-cache: true
35
+
36
+ - name: Run ruff check
37
+ run: uv run ruff check .
38
+
39
+ - name: Run type checking
40
+ run: uvx ty check . --ignore no-matching-overload
@@ -49,12 +49,5 @@ jobs:
49
49
 
50
50
  - name: Run tests
51
51
  if: steps.cache-pytest.outputs.cache-hit != 'true'
52
- run: uv run pytest .
53
-
54
- - name: Run ruff check
55
- if: steps.cache-pytest.outputs.cache-hit != 'true'
56
- run: uv run ruff check .
57
-
58
- - name: Run type checking
59
- if: steps.cache-pytest.outputs.cache-hit != 'true'
60
- run: uvx ty check . --ignore no-matching-overload
52
+ # Avoid executing examples in CI; they could run untrusted code from PRs
53
+ run: uv run pytest . --ignore=tests/test_examples_run.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: confkit
3
- Version: 1.3.0
3
+ Version: 2.0.0
4
4
  Summary: Lightweight and Easy to use configuration manager for Python projects
5
5
  Author: HEROgold
6
6
  Requires-Python: >=3.11
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "confkit"
3
- version = "1.3.0"
3
+ version = "2.0.0"
4
4
  description = "Lightweight and Easy to use configuration manager for Python projects"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,11 +1,17 @@
1
- lint.select = ["ALL"]
2
- lint.ignore = ["D203", "G004"]
3
1
  line-length = 128
4
2
  target-version = "py311"
5
3
 
4
+ [lint]
5
+ select = ["ALL"]
6
+ ignore = ["D203", "G004"]
7
+
8
+ [lint.isort]
9
+ required-imports = ["from __future__ import annotations"]
10
+
6
11
  [lint.per-file-ignores]
7
12
  "test_*.py" = ["S101", "D103", "SLF001", "D100","D101", "FBT003", "PLR2004", "N814", "S105"]
8
13
  "examples/*.py" = ["ALL"]
14
+ "src/confkit/data_types.py" = ["E701"] # Allow inline return after : on match for readability
9
15
 
10
16
  [format]
11
17
  # Don't apply line-length formatting to comments
@@ -2,6 +2,7 @@
2
2
 
3
3
  It includes the Config class and various data types used for configuration values.
4
4
  """
5
+ from __future__ import annotations
5
6
 
6
7
  from .config import Config, ConfigContainerMeta
7
8
  from .data_types import (
@@ -60,6 +60,7 @@ class Config(Generic[VT]):
60
60
  _parser: ConfkitParser = UNSET
61
61
  _file: Path = UNSET
62
62
  _has_read_config: bool = False
63
+ _data_type: BaseDataType[VT]
63
64
 
64
65
  if TYPE_CHECKING:
65
66
  # Overloads for type checkers to understand the different settings of the Config descriptors.
@@ -69,13 +70,13 @@ class Config(Generic[VT]):
69
70
  def __init__(self, default: VT) -> None: ...
70
71
  # Specify the states of optional explicitly for type checkers.
71
72
  @overload
72
- def __init__(self: Config[VT], default: VT, *, optional: Literal[False]) -> None: ...
73
+ def __init__(self: Config[OVT], default: OVT, *, optional: Literal[False]) -> None: ...
73
74
  @overload
74
- def __init__(self: Config[VT], default: BaseDataType[VT], *, optional: Literal[False]) -> None: ...
75
+ def __init__(self: Config[OVT], default: BaseDataType[OVT], *, optional: Literal[False]) -> None: ...
75
76
  @overload
76
- def __init__(self: Config[VT | None], default: VT, *, optional: Literal[True]) -> None: ...
77
+ def __init__(self: Config[OVT | None], default: OVT, *, optional: Literal[True]) -> None: ...
77
78
  @overload
78
- def __init__(self: Config[VT | None], default: BaseDataType[VT], *, optional: Literal[True]) -> None: ...
79
+ def __init__(self: Config[OVT | None], default: BaseDataType[OVT], *, optional: Literal[True]) -> None: ...
79
80
 
80
81
  def __init__(
81
82
  self,
@@ -144,9 +145,7 @@ class Config(Generic[VT]):
144
145
 
145
146
  def convert(self, value: str) -> VT:
146
147
  """Convert the value to the desired type using the given converter method."""
147
- # Ignore the type error of VT, type checkers don't like None as an option
148
- # We handle it using the `optional` flag, or using Optional DataType. so we can safely ignore it.
149
- return self._data_type.convert(value) # type: ignore[reportReturnType]
148
+ return self._data_type.convert(value)
150
149
 
151
150
  @staticmethod
152
151
  def _warn_base_class_usage() -> None:
@@ -6,6 +6,10 @@ import enum
6
6
  from abc import ABC, abstractmethod
7
7
  from collections.abc import Sequence
8
8
  from datetime import UTC, date, datetime, time, timedelta, tzinfo
9
+ from enum import Enum as dEnum
10
+ from enum import IntEnum as dIntEnum
11
+ from enum import IntFlag as dIntFlag
12
+ from enum import StrEnum as dStrEnum
9
13
  from typing import ClassVar, Generic, NotRequired, Required, TypedDict, TypeVar, Unpack, cast, overload
10
14
 
11
15
  from confkit.sentinels import UNSET
@@ -65,30 +69,28 @@ class BaseDataType(ABC, Generic[T]):
65
69
  return Optional(BaseDataType.cast(default))
66
70
 
67
71
  @staticmethod
68
- def cast(default: T | BaseDataType[T]) -> BaseDataType[T]:
72
+ def cast(default: T | BaseDataType[T]) -> BaseDataType[T]: # noqa: C901, PLR0911
69
73
  """Convert the default value to a BaseDataType."""
70
74
  # We use Cast to shut up type checkers, as we know primitive types will be correct.
71
75
  # If a custom type is passed, it should be a BaseDataType subclass, which already has the correct types.
76
+ # Check enum types BEFORE basic types since some enums inherit from str/int
72
77
  match default:
73
- case bool():
74
- data_type = cast("BaseDataType[T]", Boolean(default))
75
- case None:
76
- data_type = cast("BaseDataType[T]", NoneType())
77
- case int():
78
- data_type = cast("BaseDataType[T]", Integer(default))
79
- case float():
80
- data_type = cast("BaseDataType[T]", Float(default))
81
- case str():
82
- data_type = cast("BaseDataType[T]", String(default))
83
- case BaseDataType():
84
- data_type = default
78
+ case dStrEnum(): return cast("BaseDataType[T]", StrEnum(default))
79
+ case dIntFlag(): return cast("BaseDataType[T]", IntFlag(default))
80
+ case dIntEnum(): return cast("BaseDataType[T]", IntEnum(default))
81
+ case dEnum(): return cast("BaseDataType[T]", Enum(default))
82
+ case bool(): return cast("BaseDataType[T]", Boolean(default))
83
+ case None: return cast("BaseDataType[T]", NoneType())
84
+ case int(): return cast("BaseDataType[T]", Integer(default))
85
+ case float(): return cast("BaseDataType[T]", Float(default))
86
+ case str(): return cast("BaseDataType[T]", String(default))
87
+ case BaseDataType(): return default
85
88
  case _:
86
89
  msg = (
87
90
  f"Unsupported default value type: {type(default).__name__}. "
88
91
  "Use a BaseDataType subclass for custom types."
89
92
  )
90
93
  raise InvalidDefaultError(msg)
91
- return data_type
92
94
 
93
95
 
94
96
  class _EnumBase(BaseDataType[T]):
@@ -203,12 +205,17 @@ class NoneType(BaseDataType[None]):
203
205
  """Initialize the NoneType data type."""
204
206
  super().__init__(None)
205
207
 
206
- def convert(self, value: str) -> bool: # type: ignore[reportIncompatibleMethodOverride]
207
- """Convert a string value to None."""
208
- # Ignore type exception as convert should return True/False for NoneType
209
- # to determine if we have a valid null value or not.
208
+ def is_valid(self, value: str) -> bool:
209
+ """Check if the provided string value is in the set of null values."""
210
210
  return value.casefold().strip() in NoneType.null_values
211
211
 
212
+ def convert(self, value: str) -> None:
213
+ """Convert a string value to None."""
214
+ if self.is_valid(value):
215
+ return
216
+ msg = f"Value '{value}' is not a valid null value. Expected one of: {', '.join(NoneType.null_values)}."
217
+ raise ValueError(msg)
218
+
212
219
 
213
220
  class String(BaseDataType[str]):
214
221
  """A config value that is a string."""
@@ -359,8 +366,8 @@ class Optional(BaseDataType[T | None], Generic[T]):
359
366
 
360
367
  def convert(self, value: str) -> T | None:
361
368
  """Convert a string value to the optional type."""
362
- if self._none_type.convert(value):
363
- return None
369
+ if self._none_type.is_valid(value):
370
+ return self._none_type.convert(value)
364
371
  return self._data_type.convert(value)
365
372
 
366
373
  def validate(self) -> bool:
@@ -1,4 +1,6 @@
1
1
  """Module for custom exceptions used in the confkit package."""
2
+ from __future__ import annotations
3
+
2
4
 
3
5
  class InvalidDefaultError(ValueError):
4
6
  """Raised when the default value is not set or invalid."""
@@ -4,5 +4,6 @@ Modules inside this package may rely on optional extras. They are intentionally
4
4
  not imported eagerly so users can access the pieces they installed without
5
5
  pulling in additional dependencies.
6
6
  """
7
+ from __future__ import annotations
7
8
 
8
9
  __all__: list[str] = []
@@ -1,7 +1,13 @@
1
1
  """Helper utilities for working with Pydantic models and confkit."""
2
+ from __future__ import annotations
2
3
 
3
- try:
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
4
7
  from pydantic import BaseModel
8
+
9
+ try:
10
+ import pydantic # noqa: F401
5
11
  except ImportError as exc:
6
12
  msg = (
7
13
  "confkit.ext.pydantic requires the optional 'pydantic' extra. "
@@ -2,6 +2,7 @@
2
2
 
3
3
  It allows you to create relative references to other configuration files
4
4
  """
5
+ from __future__ import annotations
5
6
 
6
7
  from pathlib import Path
7
8
  from typing import Any
@@ -177,7 +177,6 @@ class EnvParser(ConfkitParser):
177
177
  @override
178
178
  def set_section(self, section: str) -> None:
179
179
  """EnvParser has no sections, this is a no-op."""
180
- pass # noqa: PIE790
181
180
 
182
181
  @override
183
182
  def set_option(self, option: str) -> None:
@@ -188,7 +187,6 @@ class EnvParser(ConfkitParser):
188
187
  @override
189
188
  def add_section(self, section: str) -> None:
190
189
  """EnvParser has no sections, this is a no-op."""
191
- pass # noqa: PIE790
192
190
 
193
191
  @override
194
192
  def set(self, section: str, option: str, value: object) -> None:
@@ -1,4 +1,5 @@
1
1
  """Private sentinel for missing values."""
2
+ from __future__ import annotations
2
3
 
3
4
  from typing import Any
4
5
 
@@ -1,5 +1,10 @@
1
1
  """A simple file watcher to monitor changes in a file."""
2
- from pathlib import Path
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from pathlib import Path
3
8
 
4
9
 
5
10
  class FileWatcher: # noqa: D101
@@ -1,4 +1,6 @@
1
1
  """Test suite for the Config class and its descriptors."""
2
+ from __future__ import annotations
3
+
2
4
  import enum
3
5
  from enum import auto
4
6
  from pathlib import Path
@@ -105,10 +107,10 @@ class Test:
105
107
  binary_value = Config(Binary(0b101010))
106
108
  binary_value_2 = Config(Binary(b"101010"))
107
109
  # Test invalid setups (Type checkers like pyright will raise errors here)
108
- none_int = Config(Integer(None)) # type: ignore[reportArgumentType]
109
- none_string = Config(String(None)) # type: ignore[reportArgumentType]
110
- none_boolean = Config(Boolean(None)) # type: ignore[reportArgumentType]
111
- none_float = Config(Float(None)) # type: ignore[reportArgumentType]
110
+ none_int = Config(Integer(None))
111
+ none_string = Config(String(None))
112
+ none_boolean = Config(Boolean(None))
113
+ none_float = Config(Float(None))
112
114
  # Custom data type tests
113
115
  custom = Config(Integer(0))
114
116
  optional_custom = Config(Optional(Integer(0)))
@@ -153,7 +155,7 @@ class Test:
153
155
  list_of_paths = Config(List(["/path/to/file1", "/path/to/file2"]))
154
156
 
155
157
  @Config.with_setting(number)
156
- def setting(self, **kwargs): # type: ignore[reportMissingParameterType] # noqa: ANN003, ANN201, D102
158
+ def setting(self, **kwargs): # noqa: ANN003, ANN201, D102
157
159
  return kwargs.get("number")
158
160
 
159
161
  @pytest.mark.order(0)
@@ -183,19 +185,19 @@ def test_int_flag() -> None:
183
185
 
184
186
  def test_init_no_args() -> None:
185
187
  with pytest.raises((InvalidDefaultError, InvalidConverterError)):
186
- Config() # type: ignore[reportCallIssue]
188
+ Config()
187
189
 
188
190
 
189
191
  def test_init_no_default() -> None:
190
192
  with pytest.raises(InvalidDefaultError):
191
- Config() # type: ignore[reportCallIssue]
193
+ Config()
192
194
 
193
195
 
194
196
  def test_optional_validate_none_value() -> None:
195
197
  """Test Optional.validate when value is None."""
196
198
  optional_type = Optional(String("default"))
197
199
  # Use monkey patching to set internal state
198
- with patch.object(optional_type._data_type, "value", None): # type: ignore[attr-defined]
200
+ with patch.object(optional_type._data_type, "value", None):
199
201
  assert optional_type.validate() is True
200
202
 
201
203
 
@@ -2,10 +2,11 @@
2
2
 
3
3
  This also contains the test cases where specific settings are expected.
4
4
  """
5
+ from __future__ import annotations
6
+
5
7
  import tempfile
6
- from collections.abc import Callable
7
8
  from pathlib import Path
8
- from typing import Never, ParamSpec, TypeVar
9
+ from typing import TYPE_CHECKING, Never, ParamSpec, TypeVar
9
10
 
10
11
  import pytest
11
12
  from hypothesis import given
@@ -17,6 +18,9 @@ from confkit.exceptions import InvalidConverterError, InvalidDefaultError
17
18
  from confkit.parsers import IniParser
18
19
  from confkit.sentinels import UNSET
19
20
 
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Callable
23
+
20
24
  F = TypeVar("F")
21
25
  P = ParamSpec("P")
22
26
 
@@ -60,7 +64,7 @@ def test_config_validate_parser_unset() -> None:
60
64
  def test_config_converter_is_unset() -> None:
61
65
  """Test validate_strict_type when converter is UNSET - Line 154 in config.py."""
62
66
  class MockDataType(BaseDataType[str]):
63
- def convert(self, value: str) -> Never: # ty: ignore[invalid-return-type]
67
+ def convert(self, value: str) -> Never:
64
68
  ...
65
69
 
66
70
  # Create a temporary isolated environment
@@ -148,7 +152,7 @@ def test_config_type_mismatch_error() -> None:
148
152
  class WrongTypeDataType(BaseDataType[str]):
149
153
  def __init__(self, default: str) -> None:
150
154
  super().__init__(default)
151
- def convert(self, value: str) -> int: # type: ignore[override]
155
+ def convert(self, value: str) -> int:
152
156
  return int(value)
153
157
 
154
158
  # Create a temporary isolated environment
@@ -3,9 +3,10 @@
3
3
  These are usually not safe enough to test using a single file, at the same time.ArithmeticError
4
4
  These get their own test file.
5
5
  """
6
- from collections.abc import Callable
6
+ from __future__ import annotations
7
+
7
8
  from pathlib import Path
8
- from typing import ParamSpec, TypeVar
9
+ from typing import TYPE_CHECKING, ParamSpec, TypeVar
9
10
 
10
11
  import pytest
11
12
  from hypothesis import given
@@ -15,6 +16,9 @@ from confkit.config import Config as OG
15
16
  from confkit.parsers import IniParser
16
17
  from confkit.sentinels import UNSET
17
18
 
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Callable
21
+
18
22
  F = TypeVar("F")
19
23
  P = ParamSpec("P")
20
24
 
@@ -25,7 +29,7 @@ def config_new(func: Callable[P, F]) -> Callable[P, F]:
25
29
  """Save and restore the _file and _parser attributes for the Config."""
26
30
  def inner(*args: P.args, **kwargs: P.kwargs) -> F:
27
31
  restores = (getattr(Config, "_file", UNSET), getattr(Config, "_parser", UNSET))
28
- new_file = Path(f"{func.__name__}.ini") # ty: ignore[unresolved-attribute]
32
+ new_file = Path(f"{func.__name__}.ini")
29
33
  new_file.touch(exist_ok=True)
30
34
  Config._file = new_file
31
35
  Config._parser = IniParser()
@@ -1,4 +1,6 @@
1
1
  """Test Config.detect_parser behavior for different file extensions."""
2
+ from __future__ import annotations
3
+
2
4
  from pathlib import Path
3
5
 
4
6
  import pytest
@@ -1,4 +1,6 @@
1
1
  """Tests for data type classes in confkit.data_types."""
2
+ from __future__ import annotations
3
+
2
4
  from datetime import UTC, date, datetime, time, timedelta
3
5
  from typing import Final
4
6
 
@@ -1,4 +1,6 @@
1
1
  """Tests for enum types displaying allowed values."""
2
+ from __future__ import annotations
3
+
2
4
  import enum
3
5
  from enum import IntEnum, IntFlag, StrEnum, auto
4
6
 
@@ -0,0 +1,146 @@
1
+ """Tests for automatic enum type conversion in Config descriptors.
2
+
3
+ Tests that StrEnum, IntEnum, IntFlag, and Enum defaults are automatically
4
+ wrapped in their corresponding data type converters when used with Config.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import enum
9
+ from enum import IntEnum, IntFlag, StrEnum
10
+ from typing import TYPE_CHECKING
11
+
12
+ from confkit import Config as ConfigBase
13
+ from confkit.data_types import Enum as ConfigEnum
14
+ from confkit.data_types import IntEnum as ConfigIntEnum
15
+ from confkit.data_types import IntFlag as ConfigIntFlag
16
+ from confkit.data_types import StrEnum as ConfigStrEnum
17
+ from confkit.parsers import IniParser
18
+
19
+ if TYPE_CHECKING:
20
+ from pathlib import Path
21
+
22
+
23
+ class LogLevel(StrEnum):
24
+ """String-based enum for log levels."""
25
+
26
+ DEBUG = "debug"
27
+ INFO = "info"
28
+ WARNING = "warning"
29
+ ERROR = "error"
30
+
31
+
32
+ class Priority(IntEnum):
33
+ """Integer-based enum for task priorities."""
34
+
35
+ LOW = 0
36
+ MEDIUM = 5
37
+ HIGH = 10
38
+
39
+
40
+ class Permission(IntFlag):
41
+ """Integer flag enum for permission bits."""
42
+
43
+ NONE = 0
44
+ READ = 1
45
+ WRITE = 2
46
+ EXECUTE = 4
47
+ ALL = READ | WRITE | EXECUTE
48
+
49
+
50
+ class StandardEnum(enum.Enum):
51
+ """Standard enum for testing."""
52
+
53
+ OPTION_A = 1
54
+ OPTION_B = 2
55
+
56
+
57
+ class TestStrEnumAutoWrapping:
58
+ """Test that StrEnum instances are automatically wrapped."""
59
+
60
+ def test_strenum_auto_wraps_to_config_strenum(self, tmp_path: Path) -> None:
61
+ """Test that Config(StrEnum.value) auto-wraps to Config(ConfigStrEnum(StrEnum.value))."""
62
+ config_file = tmp_path / "config.ini"
63
+ config_file.write_text("")
64
+
65
+ class StrEnumConfig(ConfigBase):
66
+ pass
67
+
68
+ StrEnumConfig.set_parser(IniParser())
69
+ StrEnumConfig.set_file(config_file)
70
+ StrEnumConfig._has_read_config = False
71
+
72
+ class AppConfig:
73
+ log_level = StrEnumConfig(LogLevel.INFO)
74
+
75
+ descriptor = AppConfig.__dict__["log_level"]
76
+ assert isinstance(descriptor._data_type, ConfigStrEnum)
77
+ assert descriptor._data_type.value == LogLevel.INFO
78
+
79
+
80
+ class TestIntEnumAutoWrapping:
81
+ """Test that IntEnum instances are automatically wrapped."""
82
+
83
+ def test_intenum_auto_wraps_to_config_intenum(self, tmp_path: Path) -> None:
84
+ """Test that Config(IntEnum.value) auto-wraps to Config(ConfigIntEnum(IntEnum.value))."""
85
+ config_file = tmp_path / "config.ini"
86
+ config_file.write_text("")
87
+
88
+ class IntEnumConfig(ConfigBase):
89
+ pass
90
+
91
+ IntEnumConfig.set_parser(IniParser())
92
+ IntEnumConfig.set_file(config_file)
93
+ IntEnumConfig._has_read_config = False
94
+
95
+ class AppConfig:
96
+ priority = IntEnumConfig(Priority.MEDIUM)
97
+
98
+ descriptor = AppConfig.__dict__["priority"]
99
+ assert isinstance(descriptor._data_type, ConfigIntEnum)
100
+ assert descriptor._data_type.value == Priority.MEDIUM
101
+
102
+
103
+ class TestIntFlagAutoWrapping:
104
+ """Test that IntFlag instances are automatically wrapped."""
105
+
106
+ def test_intflag_auto_wraps_to_config_intflag(self, tmp_path: Path) -> None:
107
+ """Test that Config(IntFlag.value) auto-wraps to Config(ConfigIntFlag(IntFlag.value))."""
108
+ config_file = tmp_path / "config.ini"
109
+ config_file.write_text("")
110
+
111
+ class IntFlagConfig(ConfigBase):
112
+ pass
113
+
114
+ IntFlagConfig.set_parser(IniParser())
115
+ IntFlagConfig.set_file(config_file)
116
+ IntFlagConfig._has_read_config = False
117
+
118
+ class AppConfig:
119
+ perms = IntFlagConfig(Permission.READ)
120
+
121
+ descriptor = AppConfig.__dict__["perms"]
122
+ assert isinstance(descriptor._data_type, ConfigIntFlag)
123
+ assert descriptor._data_type.value == Permission.READ
124
+
125
+
126
+ class TestStandardEnumAutoWrapping:
127
+ """Test that standard Enum instances are automatically wrapped."""
128
+
129
+ def test_standard_enum_auto_wraps_to_config_enum(self, tmp_path: Path) -> None:
130
+ """Test that Config(Enum.value) auto-wraps to Config(ConfigEnum(Enum.value))."""
131
+ config_file = tmp_path / "config.ini"
132
+ config_file.write_text("")
133
+
134
+ class EnumConfig(ConfigBase):
135
+ pass
136
+
137
+ EnumConfig.set_parser(IniParser())
138
+ EnumConfig.set_file(config_file)
139
+ EnumConfig._has_read_config = False
140
+
141
+ class AppConfig:
142
+ option = EnumConfig(StandardEnum.OPTION_A)
143
+
144
+ descriptor = AppConfig.__dict__["option"]
145
+ assert isinstance(descriptor._data_type, ConfigEnum)
146
+ assert descriptor._data_type.value == StandardEnum.OPTION_A
@@ -1,4 +1,5 @@
1
1
  """Test the List data type."""
2
+ from __future__ import annotations
2
3
 
3
4
  import pytest
4
5
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pathlib import Path
2
4
 
3
5
  from confkit.config import Config as OG
@@ -1,5 +1,7 @@
1
1
 
2
2
  """Tests for MsgspecParser (unified YAML/JSON/TOML parser)."""
3
+ from __future__ import annotations
4
+
3
5
  import pathlib
4
6
  import tempfile
5
7
 
@@ -1,4 +1,6 @@
1
1
  """Tests for MsgspecParser.get fallback and string branch in write."""
2
+ from __future__ import annotations
3
+
2
4
  import io
3
5
 
4
6
  from confkit.ext.parsers import MsgspecParser
@@ -1,4 +1,6 @@
1
1
  """Test MsgspecParser behavior when msgspec is not installed."""
2
+ from __future__ import annotations
3
+
2
4
  import re
3
5
  import sys
4
6
 
@@ -1,11 +1,16 @@
1
1
  """Tests for MsgspecParser.read edge cases."""
2
- from pathlib import Path
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING
3
5
 
4
6
  import msgspec
5
7
  import pytest
6
8
 
7
9
  from confkit.ext.parsers import MsgspecParser
8
10
 
11
+ if TYPE_CHECKING:
12
+ from pathlib import Path
13
+
9
14
 
10
15
  def test_read_file_does_not_exist(tmp_path: Path) -> None:
11
16
  file = tmp_path / "notfound.json"
@@ -1,4 +1,6 @@
1
1
  """Tests for MsgspecParser section/option methods."""
2
+ from __future__ import annotations
3
+
2
4
  from confkit.ext.parsers import MsgspecParser
3
5
 
4
6
 
@@ -1,4 +1,6 @@
1
1
  """Tests for multiple configurations."""
2
+ from __future__ import annotations
3
+
2
4
  from pathlib import Path
3
5
 
4
6
  import hypothesis
@@ -1,4 +1,6 @@
1
1
  """Tests for on_file_change method in Config descriptor."""
2
+ from __future__ import annotations
3
+
2
4
  from pathlib import Path
3
5
  from typing import Any, ClassVar
4
6
 
@@ -1,4 +1,5 @@
1
1
  """Test cases for the UNSET sentinel from confkit.sentinels."""
2
+ from __future__ import annotations
2
3
 
3
4
  from confkit.sentinels import UNSET
4
5
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pathlib import Path
2
4
  from tempfile import TemporaryDirectory
3
5
 
@@ -1,4 +1,5 @@
1
1
  """Test suite for testing supposedly unreachable code paths in data_types.py."""
2
+ from __future__ import annotations
2
3
 
3
4
  import pytest
4
5
  from hypothesis import given
@@ -10,7 +11,7 @@ from confkit.data_types import BaseDataType
10
11
  class DataType(BaseDataType[str]):
11
12
  """Basic DataType that doesn't do anything."""
12
13
 
13
- def convert(self, value: str) -> str: ... # ty: ignore[invalid-return-type]. # noqa: D102
14
+ def convert(self, value: str) -> str: ... # noqa: D102
14
15
 
15
16
  class MockBase:
16
17
  """Mock base class without __args__."""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes