confkit 2.1.1__tar.gz → 2.2.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-2.1.1 → confkit-2.2.0}/.pre-commit-config.yaml +5 -5
- {confkit-2.1.1 → confkit-2.2.0}/PKG-INFO +4 -4
- confkit-2.2.0/docs/examples/delegate.md +67 -0
- {confkit-2.1.1 → confkit-2.2.0}/docs/examples/index.md +1 -0
- confkit-2.2.0/examples/delegate.py +32 -0
- {confkit-2.1.1 → confkit-2.2.0}/mkdocs.yml +1 -0
- {confkit-2.1.1 → confkit-2.2.0}/pyproject.toml +17 -17
- {confkit-2.1.1 → confkit-2.2.0}/src/confkit/config.py +2 -2
- {confkit-2.1.1 → confkit-2.2.0}/src/confkit/data_types.py +1 -1
- confkit-2.2.0/src/confkit/ext/delegate.py +143 -0
- confkit-2.2.0/tests/test_delegate.py +168 -0
- {confkit-2.1.1 → confkit-2.2.0}/uv.lock +2 -7
- {confkit-2.1.1 → confkit-2.2.0}/zensical.toml +1 -0
- {confkit-2.1.1 → confkit-2.2.0}/.bumpversion.toml +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/.github/FUNDING.yml +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/.github/copilot-instructions.md +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/.github/dependabot.yml +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/.github/workflows/bump-version-by-labels.yml +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/.github/workflows/coverage.yml +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/.github/workflows/docs.yml +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/.github/workflows/gitleaks.yml +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/.github/workflows/lint-public.yml +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/.github/workflows/lint.yml +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/.github/workflows/pypi.yml +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/.github/workflows/release.yml +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/.github/workflows/test.yml +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/.gitignore +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/.python-version +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/.vscode/settings.json +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/LICENSE +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/README.md +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/docs/examples/argparse.md +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/docs/examples/basic.md +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/docs/examples/custom_data_type.md +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/docs/examples/data_types.md +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/docs/examples/decorators.md +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/docs/examples/enums.md +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/docs/examples/list_types.md +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/docs/examples/optional_values.md +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/docs/index.md +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/docs/markdown.md +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/docs/reference/config.md +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/docs/reference/data_types.md +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/docs/reference/exceptions.md +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/docs/reference/notes_two_instances.md +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/docs/usage.md +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/api.ini +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/argparse_example.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/args.ini +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/basic.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/config.ini +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/custom_data_type.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/data_types.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/database.ini +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/decorators.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/enums.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/example.json +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/example.toml +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/example.yaml +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/file_change_event.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/list_types.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/multiple_configs.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/nested_config.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/nested_example.ini +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/nested_example.json +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/nested_example.toml +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/nested_example.yaml +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/optional_values.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/other_file_types.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/pydantic_example.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/references.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/examples/url_example.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/ruff.toml +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/src/confkit/__init__.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/src/confkit/exceptions.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/src/confkit/ext/__init__.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/src/confkit/ext/parsers.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/src/confkit/ext/pydantic.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/src/confkit/ext/reference.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/src/confkit/parsers.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/src/confkit/py.typed +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/src/confkit/sentinels.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/src/confkit/watcher.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/__init__.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/conftest.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_config.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_config_classvars.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_config_decorators.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_config_detect_parser.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_data_types.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_enum_allowed_values.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_enum_auto_conversion.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_env_parser.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_examples_run.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_list_type.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_metaclass.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_msgspec.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_msgspecparser_branches.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_msgspecparser_no_msgspec.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_msgspecparser_read.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_msgspecparser_sections.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_multiple_configurations.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_nested_config.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_on_file_change.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_pydantic_models.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_sentinel.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_two_instances.py +0 -0
- {confkit-2.1.1 → confkit-2.2.0}/tests/test_unreachable_paths.py +0 -0
|
@@ -6,7 +6,7 @@ default_install_hook_types:
|
|
|
6
6
|
|
|
7
7
|
repos:
|
|
8
8
|
- repo: https://github.com/astral-sh/uv-pre-commit
|
|
9
|
-
rev: 0.
|
|
9
|
+
rev: 0.13.5
|
|
10
10
|
hooks:
|
|
11
11
|
- id: uv-lock
|
|
12
12
|
stages:
|
|
@@ -51,19 +51,19 @@ repos:
|
|
|
51
51
|
|
|
52
52
|
# Security scanning
|
|
53
53
|
- repo: https://github.com/gitleaks/gitleaks
|
|
54
|
-
rev: v8.
|
|
54
|
+
rev: v8.30.0
|
|
55
55
|
hooks:
|
|
56
56
|
- id: gitleaks
|
|
57
57
|
|
|
58
58
|
# Validate packaging metadata early
|
|
59
59
|
- repo: https://github.com/abravalheri/validate-pyproject
|
|
60
|
-
rev: v0.
|
|
60
|
+
rev: v0.27
|
|
61
61
|
hooks:
|
|
62
62
|
- id: validate-pyproject
|
|
63
63
|
stages: [pre-push, pre-commit]
|
|
64
64
|
|
|
65
65
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
66
|
-
rev: v0.
|
|
66
|
+
rev: v0.17.2
|
|
67
67
|
hooks:
|
|
68
68
|
- id: ruff
|
|
69
69
|
name: ruff lint
|
|
@@ -97,7 +97,7 @@ repos:
|
|
|
97
97
|
|
|
98
98
|
# Spell checking (global)
|
|
99
99
|
- repo: https://github.com/shellcheck-py/shellcheck-py
|
|
100
|
-
rev: v0.11.0.1
|
|
100
|
+
rev: v0.11.0.1-1
|
|
101
101
|
hooks:
|
|
102
102
|
- id: shellcheck
|
|
103
103
|
stages: [pre-push, pre-commit]
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: confkit
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Lightweight and Easy to use configuration manager for Python projects
|
|
5
5
|
Author: HEROgold
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Requires-Python: >=3.11
|
|
8
|
-
Requires-Dist: typing-extensions>=4.
|
|
8
|
+
Requires-Dist: typing-extensions>=4.16.0
|
|
9
9
|
Provides-Extra: msgspec
|
|
10
|
-
Requires-Dist: msgspec>=0.
|
|
10
|
+
Requires-Dist: msgspec>=0.22.0; extra == 'msgspec'
|
|
11
11
|
Requires-Dist: pyyaml>=6.0.3; extra == 'msgspec'
|
|
12
12
|
Requires-Dist: tomli-w>=1.2.0; extra == 'msgspec'
|
|
13
13
|
Provides-Extra: pydantic
|
|
14
|
-
Requires-Dist: pydantic>=2.
|
|
14
|
+
Requires-Dist: pydantic>=2.14.2; extra == 'pydantic'
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
|
|
17
17
|
# confkit
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Delegate Example ([`delegate.py`](https://github.com/HEROgold/confkit/blob/master/examples/delegate.py))
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Shows how one setting can read and write a setting in another config file, so that file holds the only copy of the value.
|
|
6
|
+
|
|
7
|
+
## Key Concepts
|
|
8
|
+
|
|
9
|
+
- `Delegate(Settings.dashboard_url)` passes reads and writes through to `Settings.dashboard_url`
|
|
10
|
+
- The delegate stores nothing, so its own file gets no key for it
|
|
11
|
+
- Type checkers see the delegate with the target's type
|
|
12
|
+
|
|
13
|
+
## Running
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv run python examples/delegate.py
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Code
|
|
20
|
+
|
|
21
|
+
The bot and the dashboard both read `app.ini`. Only the dashboard reads `dashboard.ini`. The dashboard's public address lives in `app.ini`, and `dashboard.ini` refers to it:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import TypeVar
|
|
26
|
+
|
|
27
|
+
from confkit.config import Config
|
|
28
|
+
from confkit.ext.delegate import Delegate
|
|
29
|
+
|
|
30
|
+
T = TypeVar("T")
|
|
31
|
+
class AppConfig(Config[T]): ...
|
|
32
|
+
class DashboardConfig(Config[T]): ...
|
|
33
|
+
|
|
34
|
+
AppConfig.set_file(Path("app.ini"))
|
|
35
|
+
DashboardConfig.set_file(Path("dashboard.ini"))
|
|
36
|
+
|
|
37
|
+
class Settings:
|
|
38
|
+
dashboard_url = AppConfig("http://localhost:8001")
|
|
39
|
+
|
|
40
|
+
class DashboardSettings:
|
|
41
|
+
session_key = DashboardConfig("change-me")
|
|
42
|
+
public_url = Delegate(Settings.dashboard_url)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Generated Files
|
|
46
|
+
|
|
47
|
+
`app.ini` holds the value:
|
|
48
|
+
|
|
49
|
+
```ini
|
|
50
|
+
[Settings]
|
|
51
|
+
dashboard_url = http://localhost:8001
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`dashboard.ini` has no `public_url` key:
|
|
55
|
+
|
|
56
|
+
```ini
|
|
57
|
+
[DashboardSettings]
|
|
58
|
+
session_key = change-me
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Notes
|
|
62
|
+
|
|
63
|
+
- Reading `DashboardSettings.public_url` reads the `dashboard_url` key in `app.ini`.
|
|
64
|
+
- `DashboardSettings().public_url = "https://dashboard.example.com"` writes the `dashboard_url` key in `app.ini`.
|
|
65
|
+
- A delegate can point at another delegate. Both read and write the same key.
|
|
66
|
+
- Pass a plain attribute read of a `Config` setting, like `Settings.dashboard_url` or `module.Settings.dashboard_url`. A literal or a computed value such as `Settings.dashboard_url + "/callback"` raises `TypeError`.
|
|
67
|
+
- `Delegate` finds the target by reading the caller's bytecode, so it runs on CPython only.
|
|
@@ -29,5 +29,6 @@ uv run python examples/basic.py
|
|
|
29
29
|
| Enums | [Enums](enums.md) | StrEnum, IntEnum, IntFlag, optional enum values |
|
|
30
30
|
| Decorators | [Decorators](decorators.md) | Injecting config into functions |
|
|
31
31
|
| argparse Integration | [Argparse](argparse.md) | CLI defaults + config separation |
|
|
32
|
+
| Delegates | [Delegate](delegate.md) | Reading and writing a setting in another file |
|
|
32
33
|
|
|
33
34
|
Select an example in the navigation to dive in.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import TypeVar
|
|
3
|
+
|
|
4
|
+
from confkit.config import Config
|
|
5
|
+
from confkit.ext.delegate import Delegate
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
class AppConfig(Config[T]): ...
|
|
9
|
+
class DashboardConfig(Config[T]): ...
|
|
10
|
+
|
|
11
|
+
AppConfig.set_file(Path("app.ini"))
|
|
12
|
+
DashboardConfig.set_file(Path("dashboard.ini"))
|
|
13
|
+
|
|
14
|
+
class Settings:
|
|
15
|
+
"""Read by both the bot and the dashboard."""
|
|
16
|
+
|
|
17
|
+
dashboard_url = AppConfig("http://localhost:8001")
|
|
18
|
+
|
|
19
|
+
class DashboardSettings:
|
|
20
|
+
"""Read by the dashboard only."""
|
|
21
|
+
|
|
22
|
+
session_key = DashboardConfig("change-me")
|
|
23
|
+
public_url = Delegate(Settings.dashboard_url)
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
print("Dashboard URL:", Settings.dashboard_url)
|
|
27
|
+
print("Public URL:", DashboardSettings.public_url)
|
|
28
|
+
|
|
29
|
+
DashboardSettings().public_url = "https://dashboard.example.com"
|
|
30
|
+
print("Dashboard URL after writing the delegate:", Settings.dashboard_url)
|
|
31
|
+
assert Settings.dashboard_url == DashboardSettings.public_url
|
|
32
|
+
assert "public_url" not in Path("dashboard.ini").read_text()
|
|
@@ -17,6 +17,7 @@ nav:
|
|
|
17
17
|
- Enums: examples/enums.md
|
|
18
18
|
- Custom Data Type: examples/custom_data_type.md
|
|
19
19
|
- Argparse Integration: examples/argparse.md
|
|
20
|
+
- Delegates: examples/delegate.md
|
|
20
21
|
- API Reference:
|
|
21
22
|
- Config: reference/config.md
|
|
22
23
|
- Data Types: reference/data_types.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "confkit"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.2.0"
|
|
4
4
|
description = "Lightweight and Easy to use configuration manager for Python projects"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -8,15 +8,15 @@ authors = [
|
|
|
8
8
|
]
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
dependencies = [
|
|
11
|
-
"typing-extensions>=4.
|
|
11
|
+
"typing-extensions>=4.16.0",
|
|
12
12
|
]
|
|
13
13
|
|
|
14
14
|
[project.optional-dependencies]
|
|
15
15
|
pydantic = [
|
|
16
|
-
"pydantic>=2.
|
|
16
|
+
"pydantic>=2.14.2",
|
|
17
17
|
]
|
|
18
18
|
msgspec = [
|
|
19
|
-
"msgspec>=0.
|
|
19
|
+
"msgspec>=0.22.0",
|
|
20
20
|
"pyyaml>=6.0.3", # required by msgspec
|
|
21
21
|
"tomli-w>=1.2.0", # required by msgspec
|
|
22
22
|
]
|
|
@@ -27,27 +27,27 @@ build-backend = "hatchling.build"
|
|
|
27
27
|
|
|
28
28
|
[dependency-groups]
|
|
29
29
|
dev = [
|
|
30
|
-
"coverage>=7.
|
|
31
|
-
"pytest-cov>=7.
|
|
32
|
-
"hypothesis>=6.
|
|
33
|
-
"pydantic>=2.
|
|
34
|
-
"pytest>=9.0
|
|
35
|
-
"ruff>=0.
|
|
36
|
-
"coveralls>=4.0
|
|
37
|
-
"msgspec>=0.
|
|
30
|
+
"coverage>=7.17.0",
|
|
31
|
+
"pytest-cov>=7.1.0",
|
|
32
|
+
"hypothesis>=6.170.14",
|
|
33
|
+
"pydantic>=2.14.2",
|
|
34
|
+
"pytest>=9.2.0",
|
|
35
|
+
"ruff>=0.17.3",
|
|
36
|
+
"coveralls>=4.1.0",
|
|
37
|
+
"msgspec>=0.22.0",
|
|
38
38
|
"pyyaml>=6.0.3",
|
|
39
39
|
"tomli-w>=1.2.0",
|
|
40
|
-
"pytest-order>=1.
|
|
41
|
-
"zensical>=0.0.
|
|
40
|
+
"pytest-order>=1.5.0",
|
|
41
|
+
"zensical>=0.0.73",
|
|
42
42
|
"pdoc>=16.0.0",
|
|
43
43
|
]
|
|
44
44
|
docs = [
|
|
45
45
|
"mkdocs>=1.6.1",
|
|
46
|
-
"mkdocs-material>=9.
|
|
47
|
-
"mkdocstrings[python]>=1.0.
|
|
46
|
+
"mkdocs-material>=9.8.0",
|
|
47
|
+
"mkdocstrings[python]>=1.0.8",
|
|
48
48
|
"mkdocs-pdoc-plugin @ git+https://github.com/spirali/mkdocs-pdoc-plugin",
|
|
49
49
|
"pdoc>=16.0.0",
|
|
50
|
-
"zensical>=0.0.
|
|
50
|
+
"zensical>=0.0.73",
|
|
51
51
|
]
|
|
52
52
|
|
|
53
53
|
[[tool.pyright.executionEnvironments]]
|
|
@@ -79,7 +79,7 @@ class Config(Generic[VT]):
|
|
|
79
79
|
|
|
80
80
|
def __init__(
|
|
81
81
|
self,
|
|
82
|
-
default: VT |
|
|
82
|
+
default: VT | BaseDataType[VT] | None = UNSET,
|
|
83
83
|
*,
|
|
84
84
|
optional: bool = False,
|
|
85
85
|
) -> None:
|
|
@@ -296,7 +296,7 @@ class Config(Generic[VT]):
|
|
|
296
296
|
return inner
|
|
297
297
|
return wrapper
|
|
298
298
|
|
|
299
|
-
def _initialize_data_type(self, default: VT |
|
|
299
|
+
def _initialize_data_type(self, default: VT | BaseDataType[VT] | None) -> None:
|
|
300
300
|
"""Initialize the data type based on the default value."""
|
|
301
301
|
if not self.optional and default is not None:
|
|
302
302
|
self._data_type = BaseDataType[VT].cast(default)
|
|
@@ -69,7 +69,7 @@ class BaseDataType(ABC, Generic[T]):
|
|
|
69
69
|
raise TypeError(msg)
|
|
70
70
|
|
|
71
71
|
@staticmethod
|
|
72
|
-
def cast_optional(default: T |
|
|
72
|
+
def cast_optional(default: T | BaseDataType[T] | None) -> BaseDataType[T | None]:
|
|
73
73
|
"""Convert the default value to an Optional data type."""
|
|
74
74
|
if default is None:
|
|
75
75
|
return cast("BaseDataType[T | None]", NoneType())
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Extension to confkit adds delegate-related functionality.
|
|
2
|
+
|
|
3
|
+
A `Delegate` passes reads and writes through to a setting declared elsewhere, usually in another config file.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
class Settings:
|
|
7
|
+
dashboard_url = AppConfig("http://localhost:8001")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DashboardSettings:
|
|
11
|
+
public_url = Delegate(Settings.dashboard_url)
|
|
12
|
+
```
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import dis
|
|
18
|
+
import sys
|
|
19
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
20
|
+
|
|
21
|
+
from confkit.config import Config
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from types import FrameType
|
|
25
|
+
|
|
26
|
+
T = TypeVar("T")
|
|
27
|
+
|
|
28
|
+
_NAME_LOADS = frozenset(
|
|
29
|
+
{
|
|
30
|
+
"LOAD_NAME",
|
|
31
|
+
"LOAD_GLOBAL",
|
|
32
|
+
"LOAD_DEREF",
|
|
33
|
+
"LOAD_CLASSDEREF", # 3.11, class body reading a variable of the enclosing function.
|
|
34
|
+
"LOAD_FROM_DICT_OR_DEREF", # 3.12+, same as above.
|
|
35
|
+
"LOAD_FAST",
|
|
36
|
+
"LOAD_FAST_CHECK",
|
|
37
|
+
"LOAD_FAST_BORROW",
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
_SKIPPED = frozenset({"PRECALL", "EXTENDED_ARG"}) # PRECALL only exists on 3.11.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Delegate(Generic[T]):
|
|
44
|
+
"""Descriptor that passes reads and writes through to another setting.
|
|
45
|
+
|
|
46
|
+
Pass the target setting as a plain attribute read, like `Delegate(Settings.dashboard_url)`.
|
|
47
|
+
`Settings.dashboard_url` evaluates to its value before `Delegate` runs, so `Delegate`
|
|
48
|
+
reads the caller's CPython bytecode to find the expression and looks up the `Config` descriptor it names.
|
|
49
|
+
|
|
50
|
+
The delegate stores nothing. Reading it reads the target's key, and assigning through
|
|
51
|
+
an instance writes the target's key, in the target's file.
|
|
52
|
+
A delegate of a delegate passes through to the same `Config` descriptor.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, target: T, /) -> None: # noqa: ARG002 The value is only there for type checkers.
|
|
56
|
+
"""Find the `Config` descriptor named by the argument expression."""
|
|
57
|
+
owner, target_descriptor = _find_target(_caller_frame())
|
|
58
|
+
self._owner: type = owner
|
|
59
|
+
self._target: Config[T] = target_descriptor
|
|
60
|
+
|
|
61
|
+
def __get__(self, obj: object, obj_type: type | None = None) -> T:
|
|
62
|
+
"""Read the target setting."""
|
|
63
|
+
return self._target.__get__(None, self._owner)
|
|
64
|
+
|
|
65
|
+
def __set__(self, obj: object, value: T) -> None:
|
|
66
|
+
"""Write the target setting.
|
|
67
|
+
|
|
68
|
+
The target's owner class is passed on, so `Config.__set__` never touches the delegating instance.
|
|
69
|
+
"""
|
|
70
|
+
self._target.__set__(self._owner, value)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _caller_frame() -> FrameType:
|
|
74
|
+
"""Return the frame that called `Delegate(...)`.
|
|
75
|
+
|
|
76
|
+
`Delegate[str](...)` goes through `typing`'s generic alias, which adds its own frames.
|
|
77
|
+
"""
|
|
78
|
+
frame = sys._getframe(2) # noqa: SLF001 Skip this function and Delegate.__init__.
|
|
79
|
+
while frame.f_globals.get("__name__") == "typing" and frame.f_back is not None:
|
|
80
|
+
frame = frame.f_back
|
|
81
|
+
return frame
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _find_target(frame: FrameType) -> tuple[type, Config[Any]]:
|
|
85
|
+
"""Find the descriptor read by the single argument of the call running in `frame`.
|
|
86
|
+
|
|
87
|
+
Walks back from the CALL instruction over the LOAD_ATTR chain to the name it starts from,
|
|
88
|
+
for example LOAD_NAME Settings, LOAD_ATTR dashboard_url.
|
|
89
|
+
"""
|
|
90
|
+
instructions = [i for i in dis.get_instructions(frame.f_code) if i.opname not in _SKIPPED]
|
|
91
|
+
call = next((n for n, i in enumerate(instructions) if i.offset == frame.f_lasti), None)
|
|
92
|
+
attrs: list[str] = []
|
|
93
|
+
base: str | None = None
|
|
94
|
+
if call is not None and instructions[call].opname == "CALL" and instructions[call].arg == 1:
|
|
95
|
+
for instruction in reversed(instructions[:call]):
|
|
96
|
+
if instruction.opname == "LOAD_ATTR":
|
|
97
|
+
attrs.append(instruction.argval)
|
|
98
|
+
continue
|
|
99
|
+
if instruction.opname in _NAME_LOADS:
|
|
100
|
+
base = instruction.argval
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
if base is None or not attrs:
|
|
104
|
+
msg = "Delegate needs a setting read as its only argument, like Delegate(Settings.dashboard_url)."
|
|
105
|
+
raise TypeError(msg)
|
|
106
|
+
|
|
107
|
+
*path, name = reversed(attrs)
|
|
108
|
+
owner = _resolve_name(frame, base)
|
|
109
|
+
for attr in path:
|
|
110
|
+
owner = getattr(owner, attr)
|
|
111
|
+
return _lookup_descriptor(owner, name)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _resolve_name(frame: FrameType, name: str) -> object:
|
|
115
|
+
"""Look a name up the way the caller's code would.
|
|
116
|
+
|
|
117
|
+
A class body's frame doesn't expose the enclosing function's variables. For a free variable,
|
|
118
|
+
follow the frames that run the class statements out to the one that holds it.
|
|
119
|
+
"""
|
|
120
|
+
scope = frame
|
|
121
|
+
while name not in scope.f_locals and name in scope.f_code.co_freevars and scope.f_back is not None:
|
|
122
|
+
scope = scope.f_back
|
|
123
|
+
for namespace in (scope.f_locals, frame.f_globals, frame.f_builtins):
|
|
124
|
+
if name in namespace:
|
|
125
|
+
return namespace[name]
|
|
126
|
+
msg = f"Delegate could not resolve {name!r}."
|
|
127
|
+
raise NameError(msg)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _lookup_descriptor(owner: object, name: str) -> tuple[type, Config[Any]]:
|
|
131
|
+
"""Return the class that declares the `Config` descriptor `name`, and the descriptor, following delegates."""
|
|
132
|
+
cls = owner if isinstance(owner, type) else type(owner)
|
|
133
|
+
for class_ in cls.__mro__:
|
|
134
|
+
attr = vars(class_).get(name)
|
|
135
|
+
if isinstance(attr, Delegate):
|
|
136
|
+
return attr._owner, attr._target # noqa: SLF001
|
|
137
|
+
if isinstance(attr, Config):
|
|
138
|
+
return class_, attr
|
|
139
|
+
msg = f"{cls.__qualname__}.{name} is not a Config setting, so Delegate can't pass through to it."
|
|
140
|
+
raise TypeError(msg)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
__all__ = ["Delegate"]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Tests for the Delegate extension."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from types import SimpleNamespace
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from confkit.config import Config
|
|
12
|
+
from confkit.ext.delegate import Delegate
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AppConfig(Config[str]): ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DashboardConfig(Config[str]): ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
app_file = Path("delegate_app_test.ini")
|
|
22
|
+
dashboard_file = Path("delegate_dashboard_test.ini")
|
|
23
|
+
app_file.unlink(missing_ok=True)
|
|
24
|
+
dashboard_file.unlink(missing_ok=True)
|
|
25
|
+
AppConfig.set_file(app_file)
|
|
26
|
+
DashboardConfig.set_file(dashboard_file)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Settings:
|
|
30
|
+
dashboard_url = AppConfig("http://localhost:8001")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DashboardSettings:
|
|
34
|
+
secret = DashboardConfig("hunter2")
|
|
35
|
+
public_url = Delegate(Settings.dashboard_url)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Chained:
|
|
39
|
+
url = Delegate(DashboardSettings.public_url)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Explicit:
|
|
43
|
+
url = Delegate[str](Settings.dashboard_url)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Slotted:
|
|
47
|
+
__slots__ = ()
|
|
48
|
+
url = Delegate(Settings.dashboard_url)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
module = SimpleNamespace(Settings=Settings)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Dotted:
|
|
55
|
+
url = Delegate(module.Settings.dashboard_url)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_delegate_reads_target_value() -> None:
|
|
59
|
+
"""The delegate returns the target's current value."""
|
|
60
|
+
assert DashboardSettings.public_url == Settings.dashboard_url
|
|
61
|
+
assert DashboardSettings().public_url == Settings.dashboard_url
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_target_write_shows_through_delegate() -> None:
|
|
65
|
+
"""Writing the target changes what the delegate reads."""
|
|
66
|
+
Settings().dashboard_url = "https://target.example.com"
|
|
67
|
+
assert DashboardSettings.public_url == "https://target.example.com"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_delegate_write_goes_to_target() -> None:
|
|
71
|
+
"""Writing the delegate writes the target's file, not the delegate's."""
|
|
72
|
+
DashboardSettings().public_url = "https://delegate.example.com"
|
|
73
|
+
assert Settings.dashboard_url == "https://delegate.example.com"
|
|
74
|
+
assert "https://delegate.example.com" in app_file.read_text()
|
|
75
|
+
assert "public_url" not in dashboard_file.read_text()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_delegate_of_delegate_passes_through() -> None:
|
|
79
|
+
"""A delegate of a delegate reads and writes the original setting."""
|
|
80
|
+
Chained().url = "https://chained.example.com"
|
|
81
|
+
assert Settings.dashboard_url == "https://chained.example.com"
|
|
82
|
+
assert DashboardSettings.public_url == "https://chained.example.com"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_delegate_in_function_scope() -> None:
|
|
86
|
+
"""The target can be named through a local variable."""
|
|
87
|
+
owner = Settings
|
|
88
|
+
|
|
89
|
+
class Local:
|
|
90
|
+
url = Delegate(owner.dashboard_url)
|
|
91
|
+
|
|
92
|
+
Settings().dashboard_url = "https://local.example.com"
|
|
93
|
+
assert Local.url == "https://local.example.com"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_delegate_rejects_derived_value() -> None:
|
|
97
|
+
"""A value computed from a setting isn't a setting, so it raises."""
|
|
98
|
+
with pytest.raises(TypeError, match="only argument"):
|
|
99
|
+
Delegate(Settings.dashboard_url + "/callback")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_delegate_rejects_literal() -> None:
|
|
103
|
+
"""A literal isn't a setting, so it raises."""
|
|
104
|
+
with pytest.raises(TypeError, match="only argument"):
|
|
105
|
+
Delegate("http://not-a-setting")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_delegate_rejects_non_config_attribute() -> None:
|
|
109
|
+
"""An attribute that isn't a Config descriptor raises and names it."""
|
|
110
|
+
|
|
111
|
+
class Plain:
|
|
112
|
+
url = "http://plain"
|
|
113
|
+
|
|
114
|
+
with pytest.raises(TypeError, match=r"Plain\.url is not a Config setting"):
|
|
115
|
+
Delegate(Plain.url)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_delegate_with_explicit_type_parameter() -> None:
|
|
119
|
+
"""`Delegate[str](...)` finds the caller's frame past typing's generic alias."""
|
|
120
|
+
Settings().dashboard_url = "https://explicit.example.com"
|
|
121
|
+
assert Explicit.url == "https://explicit.example.com"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_delegate_rejects_keyword_argument() -> None:
|
|
125
|
+
"""The target is positional-only, so Python itself rejects the keyword form."""
|
|
126
|
+
untyped: Any = Delegate # The type checker rejects this call too; check Python's own error.
|
|
127
|
+
with pytest.raises(TypeError, match="positional-only"):
|
|
128
|
+
untyped(target=Settings.dashboard_url)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_delegate_in_class_nested_in_class_in_function() -> None:
|
|
132
|
+
"""A free variable is resolved through every enclosing class body."""
|
|
133
|
+
owner = Settings
|
|
134
|
+
|
|
135
|
+
class Outer:
|
|
136
|
+
class Inner:
|
|
137
|
+
url = Delegate(owner.dashboard_url)
|
|
138
|
+
|
|
139
|
+
Settings().dashboard_url = "https://nested.example.com"
|
|
140
|
+
assert Outer.Inner.url == "https://nested.example.com"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_delegate_write_leaves_delegating_instance_alone() -> None:
|
|
144
|
+
"""Writing through a delegate sets nothing on the delegating instance, so __slots__ classes work."""
|
|
145
|
+
Slotted().url = "https://slotted.example.com"
|
|
146
|
+
assert Settings.dashboard_url == "https://slotted.example.com"
|
|
147
|
+
|
|
148
|
+
instance = DashboardSettings()
|
|
149
|
+
instance.public_url = "https://instance.example.com"
|
|
150
|
+
assert vars(instance) == {}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_delegate_through_attribute_chain() -> None:
|
|
154
|
+
"""`module.Settings.dashboard_url` follows every attribute in the chain."""
|
|
155
|
+
Dotted().url = "https://dotted.example.com"
|
|
156
|
+
assert Settings.dashboard_url == "https://dotted.example.com"
|
|
157
|
+
assert Dotted.url == "https://dotted.example.com"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_delegate_read_runs_target_file_change_hook(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
161
|
+
"""A read through the delegate runs the target's file-change check, the same as a direct read."""
|
|
162
|
+
descriptor = vars(Settings)["dashboard_url"]
|
|
163
|
+
events: list[str] = []
|
|
164
|
+
monkeypatch.setattr(AppConfig._watcher, "has_changed", lambda: True)
|
|
165
|
+
monkeypatch.setattr(descriptor, "on_file_change", lambda origin, _old, _new: events.append(origin))
|
|
166
|
+
|
|
167
|
+
assert DashboardSettings.public_url == Settings.dashboard_url
|
|
168
|
+
assert events == ["get", "get"]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
version = 1
|
|
2
|
-
revision =
|
|
2
|
+
revision = 5
|
|
3
3
|
requires-python = ">=3.11"
|
|
4
4
|
resolution-markers = [
|
|
5
5
|
"python_full_version >= '3.15'",
|
|
@@ -169,7 +169,7 @@ wheels = [
|
|
|
169
169
|
|
|
170
170
|
[[package]]
|
|
171
171
|
name = "confkit"
|
|
172
|
-
version = "2.
|
|
172
|
+
version = "2.2.0"
|
|
173
173
|
source = { editable = "." }
|
|
174
174
|
dependencies = [
|
|
175
175
|
{ name = "typing-extensions" },
|
|
@@ -1286,13 +1286,8 @@ wheels = [
|
|
|
1286
1286
|
{ url = "https://files.pythonhosted.org/packages/49/5f/6d43ae25b6be241fd5056be8555de16c21a35b7d18321f4e204ebe1b0181/zensical-0.0.55-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b53d213126402fec053b59e68122198a92caec6e53ac9673e7b2c27008acccb7", size = 12893366, upload-time = "2026-08-16T10:19:23.907Z" },
|
|
1287
1287
|
{ url = "https://files.pythonhosted.org/packages/1f/71/df7e7a82e003da68dee0d410c510a32df67990bd4949e8e09c9260a95064/zensical-0.0.55-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:bbe0bdafab130bd5a9444adccf3330e056388eedb82681ffa51c6c476264688b", size = 12778731, upload-time = "2026-08-16T10:19:26.42Z" },
|
|
1288
1288
|
{ url = "https://files.pythonhosted.org/packages/e9/f0/c399907c672759f27e3d8031e3cdb1ff3c297f9df086d82648f573b8067e/zensical-0.0.55-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a699bc5711ddc51a7362b5875d88bcdb9eea018dd76b241920dfeff9fd14abd", size = 13225804, upload-time = "2026-08-16T10:19:28.932Z" },
|
|
1289
|
-
{ url = "https://files.pythonhosted.org/packages/e4/e5/0d50213904ef85d5412e6bcd92fe4bd88399e57bf06a28f9cba9a30a558f/zensical-0.0.55-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e5f8a734d3b1d964de4e81f257f7ddb69c79b23178df6713d56eea97e9ddbdf", size = 13159319, upload-time = "2026-08-16T10:19:31.318Z" },
|
|
1290
|
-
{ url = "https://files.pythonhosted.org/packages/66/f3/de59ba35711c9dc4a1eda435a0058245c42f72639a2399f9590c361daa7a/zensical-0.0.55-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f83fd50c61c42a9a37510843b1de07652f8ec5df8cc245382cfc2e592c7d6f83", size = 13536310, upload-time = "2026-08-16T10:19:34.026Z" },
|
|
1291
1289
|
{ url = "https://files.pythonhosted.org/packages/56/35/1e14d07c97bdb901bf4fd72e6fba52a65e1ff46d0a385b2fceeee2f65227/zensical-0.0.55-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7a970fe0f2512261d60f64ebcc9b9e2dbce4a96f520a4dac21a980881b9284d", size = 13192098, upload-time = "2026-08-16T10:19:36.247Z" },
|
|
1292
1290
|
{ url = "https://files.pythonhosted.org/packages/4f/a2/7de6478a39b23d6dbc8cd862390562d9a91641d9ca4552ed6958259e3ca0/zensical-0.0.55-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7b9b6c27080d3976ef7e2b9ac211a4f6f0a446e104a01114889a0992f571da56", size = 13403866, upload-time = "2026-08-16T10:19:38.491Z" },
|
|
1293
|
-
{ url = "https://files.pythonhosted.org/packages/73/2e/bbf25e9f3378dd2d6209dbea427cf9047cdc99533a94152c26fd33f2bb72/zensical-0.0.55-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:588b14322d55652d83de551c7fcd8a377c65753a4f393a88b359fd92738efe1e", size = 13432059, upload-time = "2026-08-16T10:19:40.931Z" },
|
|
1294
|
-
{ url = "https://files.pythonhosted.org/packages/75/5c/0400328cba6712cfce2bc12039a4184e951e4cbe1d35b12dad69b0e678d4/zensical-0.0.55-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:79e9ce4195f8d7b0a7d364f9adc1a98bdb4c5f213985edd6fa75d7e3359e8616", size = 13587117, upload-time = "2026-08-16T10:19:43.168Z" },
|
|
1295
1291
|
{ url = "https://files.pythonhosted.org/packages/84/fc/be21acae7d5b4ceb26f48b7dcda2ab65f2ad2fb865030aeff51b1a000d97/zensical-0.0.55-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bc8d2f8a343311f668c96aced77ca679144d62dc0f191dadbd9d7bc84e980295", size = 13535419, upload-time = "2026-08-16T10:19:45.584Z" },
|
|
1296
|
-
{ url = "https://files.pythonhosted.org/packages/c8/37/000e3a9cec7aad0087a353e913fd973dbe355ce3b6efdc1cce2d31764f2c/zensical-0.0.55-cp310-abi3-win32.whl", hash = "sha256:c63d6d925b69e5e115a96e6981e2c24194abda3757e65fb5ff843329f1b8bdfb", size = 12448614, upload-time = "2026-08-16T10:19:47.843Z" },
|
|
1297
1292
|
{ url = "https://files.pythonhosted.org/packages/8a/ab/f660dbc74b7acb5547c6920068516e5570965b64ae757f30847df0ab51db/zensical-0.0.55-cp310-abi3-win_amd64.whl", hash = "sha256:cbe65f11dab3f019fc047ddbf147b6f6ac8f6e2a83232c8be6d65fa7c50ee7df", size = 12712414, upload-time = "2026-08-16T10:19:50.053Z" },
|
|
1298
1293
|
]
|
|
@@ -19,6 +19,7 @@ nav = [
|
|
|
19
19
|
{ "Enums" = "examples/enums.md" },
|
|
20
20
|
{ "Custom Data Type" = "examples/custom_data_type.md" },
|
|
21
21
|
{ "Argparse Integration" = "examples/argparse.md" },
|
|
22
|
+
{ "Delegates" = "examples/delegate.md" },
|
|
22
23
|
] },
|
|
23
24
|
{ "API Reference" = [
|
|
24
25
|
{ "Config" = "reference/config.md" },
|
|
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
|
|
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
|