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.
- {confkit-1.3.0 → confkit-2.0.0}/.github/workflows/bump-version-by-labels.yml +2 -1
- confkit-2.0.0/.github/workflows/lint.yml +40 -0
- {confkit-1.3.0 → confkit-2.0.0}/.github/workflows/test.yml +2 -9
- {confkit-1.3.0 → confkit-2.0.0}/PKG-INFO +1 -1
- {confkit-1.3.0 → confkit-2.0.0}/pyproject.toml +1 -1
- {confkit-1.3.0 → confkit-2.0.0}/ruff.toml +8 -2
- {confkit-1.3.0 → confkit-2.0.0}/src/confkit/__init__.py +1 -0
- {confkit-1.3.0 → confkit-2.0.0}/src/confkit/config.py +6 -7
- {confkit-1.3.0 → confkit-2.0.0}/src/confkit/data_types.py +27 -20
- {confkit-1.3.0 → confkit-2.0.0}/src/confkit/exceptions.py +2 -0
- {confkit-1.3.0 → confkit-2.0.0}/src/confkit/ext/__init__.py +1 -0
- {confkit-1.3.0 → confkit-2.0.0}/src/confkit/ext/pydantic.py +7 -1
- {confkit-1.3.0 → confkit-2.0.0}/src/confkit/ext/reference.py +1 -0
- {confkit-1.3.0 → confkit-2.0.0}/src/confkit/parsers.py +0 -2
- {confkit-1.3.0 → confkit-2.0.0}/src/confkit/sentinels.py +1 -0
- {confkit-1.3.0 → confkit-2.0.0}/src/confkit/watcher.py +6 -1
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_config.py +10 -8
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_config_classvars.py +8 -4
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_config_decorators.py +7 -3
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_config_detect_parser.py +2 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_data_types.py +2 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_enum_allowed_values.py +2 -0
- confkit-2.0.0/tests/test_enum_auto_conversion.py +146 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_list_type.py +1 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_metaclass.py +2 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_msgspec.py +2 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_msgspecparser_branches.py +2 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_msgspecparser_no_msgspec.py +2 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_msgspecparser_read.py +6 -1
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_msgspecparser_sections.py +2 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_multiple_configurations.py +2 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_on_file_change.py +2 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_sentinel.py +1 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_two_instances.py +2 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_unreachable_paths.py +2 -1
- {confkit-1.3.0 → confkit-2.0.0}/.bumpversion.toml +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/.github/FUNDING.yml +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/.github/copilot-instructions.md +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/.github/dependabot.yml +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/.github/workflows/coverage.yml +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/.github/workflows/docs.yml +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/.github/workflows/pypi.yml +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/.github/workflows/release.yml +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/.gitignore +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/.pre-commit-config.yaml +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/.python-version +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/.vscode/settings.json +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/README.md +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/docs/examples/argparse.md +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/docs/examples/basic.md +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/docs/examples/custom_data_type.md +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/docs/examples/data_types.md +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/docs/examples/decorators.md +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/docs/examples/enums.md +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/docs/examples/index.md +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/docs/examples/list_types.md +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/docs/examples/optional_values.md +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/docs/index.md +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/docs/reference/config.md +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/docs/reference/data_types.md +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/docs/reference/exceptions.md +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/docs/reference/notes_two_instances.md +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/docs/usage.md +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/api.ini +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/argparse_example.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/args.ini +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/basic.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/config.ini +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/custom_data_type.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/data_types.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/database.ini +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/decorators.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/enums.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/example.json +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/example.toml +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/example.yaml +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/file_change_event.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/list_types.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/multiple_configs.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/nested_config.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/nested_example.ini +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/nested_example.json +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/nested_example.toml +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/nested_example.yaml +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/optional_values.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/other_file_types.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/pydantic_example.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/references.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/examples/url_example.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/mkdocs.yml +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/src/confkit/ext/parsers.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/src/confkit/py.typed +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/__init__.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/conftest.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_env_parser.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_examples_run.py +0 -0
- {confkit-1.3.0 → confkit-2.0.0}/tests/test_nested_config.py +0 -0
- {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
|
-
|
|
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,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
|
|
@@ -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[
|
|
73
|
+
def __init__(self: Config[OVT], default: OVT, *, optional: Literal[False]) -> None: ...
|
|
73
74
|
@overload
|
|
74
|
-
def __init__(self: Config[
|
|
75
|
+
def __init__(self: Config[OVT], default: BaseDataType[OVT], *, optional: Literal[False]) -> None: ...
|
|
75
76
|
@overload
|
|
76
|
-
def __init__(self: Config[
|
|
77
|
+
def __init__(self: Config[OVT | None], default: OVT, *, optional: Literal[True]) -> None: ...
|
|
77
78
|
@overload
|
|
78
|
-
def __init__(self: Config[
|
|
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
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
case
|
|
76
|
-
|
|
77
|
-
case
|
|
78
|
-
|
|
79
|
-
case
|
|
80
|
-
|
|
81
|
-
case str():
|
|
82
|
-
|
|
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
|
|
207
|
-
"""
|
|
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.
|
|
363
|
-
return
|
|
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,7 +1,13 @@
|
|
|
1
1
|
"""Helper utilities for working with Pydantic models and confkit."""
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
|
|
3
|
-
|
|
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. "
|
|
@@ -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,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))
|
|
109
|
-
none_string = Config(String(None))
|
|
110
|
-
none_boolean = Config(Boolean(None))
|
|
111
|
-
none_float = Config(Float(None))
|
|
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): #
|
|
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()
|
|
188
|
+
Config()
|
|
187
189
|
|
|
188
190
|
|
|
189
191
|
def test_init_no_default() -> None:
|
|
190
192
|
with pytest.raises(InvalidDefaultError):
|
|
191
|
-
Config()
|
|
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):
|
|
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:
|
|
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:
|
|
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
|
|
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")
|
|
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()
|
|
@@ -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,11 +1,16 @@
|
|
|
1
1
|
"""Tests for MsgspecParser.read edge cases."""
|
|
2
|
-
from
|
|
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,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: ... #
|
|
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
|
|
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
|
|
File without changes
|