liblaf-conf 0.1.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.
@@ -0,0 +1,40 @@
1
+ # ref: <https://github.com/github/gitignore/blob/main/Python.gitignore>
2
+
3
+ # Globally Useful gitignores
4
+ *.log
5
+ *.log.*
6
+
7
+ # Byte-compiled / optimized / DLL files
8
+ __pycache__/
9
+
10
+ # Distribution / packaging
11
+ dist/
12
+
13
+ # Unit test / coverage reports
14
+ .nox/
15
+ .cache/
16
+ .coverage
17
+ .coverage.*
18
+ .hypothesis/
19
+ .pytest_cache/
20
+
21
+ # pixi
22
+ .pixi/
23
+
24
+ # Environments
25
+ .venv/
26
+
27
+ # mkdocs documentation
28
+ /site/
29
+
30
+ # Ruff stuff:
31
+ .ruff_cache/
32
+
33
+ # [Codecov](https://docs.codecov.com/docs/test-analytics#1-output-a-junit-xml-file-in-your-ci)
34
+ junit.xml
35
+
36
+ # [CodSpeed](https://codspeed.io/)
37
+ .codspeed/
38
+
39
+ # [hatch-vcs](https://github.com/ofek/hatch-vcs)
40
+ _version.py
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 liblaf
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: liblaf-conf
3
+ Version: 0.1.0
4
+ Project-URL: Changelog, https://github.com/liblaf/conf/blob/main/CHANGELOG.md
5
+ Project-URL: Documentation, https://liblaf.github.io/conf/
6
+ Project-URL: Funding, https://github.com/liblaf/conf?sponsor=1
7
+ Project-URL: Homepage, https://github.com/liblaf/conf
8
+ Project-URL: Issue Tracker, https://github.com/liblaf/conf/issues
9
+ Project-URL: Release Notes, https://github.com/liblaf/conf/releases
10
+ Project-URL: Source Code, https://github.com/liblaf/conf
11
+ Author-email: liblaf <30631553+liblaf@users.noreply.github.com>
12
+ License-Expression: MIT
13
+ License-File: LICENSE
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.12
26
+ Requires-Dist: lazy-loader>=0.1
27
+ Requires-Dist: pydantic-extra-types[pendulum]>=2.11
28
+ Requires-Dist: pydantic>=2.12
29
+ Description-Content-Type: text/markdown
30
+
31
+ # conf
@@ -0,0 +1 @@
1
+ # conf
@@ -0,0 +1,99 @@
1
+ #:schema https://json.schemastore.org/pyproject.json
2
+ # ref: <https://packaging.python.org/en/latest/specifications/pyproject-toml/>
3
+
4
+ [project]
5
+ name = "liblaf-conf"
6
+ description = ""
7
+ readme = "README.md"
8
+ requires-python = ">=3.12"
9
+ license = "MIT"
10
+ authors = [
11
+ { name = "liblaf", email = "30631553+liblaf@users.noreply.github.com" }
12
+ ]
13
+ keywords = []
14
+ classifiers = [
15
+ # ref: <https://pypi.org/classifiers/>
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
26
+ "Typing :: Typed"
27
+ ]
28
+ dependencies = [
29
+ "lazy-loader>=0.1",
30
+ "pydantic>=2.12",
31
+ "pydantic-extra-types[pendulum]>=2.11"
32
+ ]
33
+ dynamic = ["version"]
34
+
35
+ [project.urls]
36
+ # ref: <https://packaging.python.org/en/latest/specifications/well-known-project-urls/#well-known-labels>
37
+ "Changelog" = "https://github.com/liblaf/conf/blob/main/CHANGELOG.md"
38
+ "Documentation" = "https://liblaf.github.io/conf/"
39
+ "Funding" = "https://github.com/liblaf/conf?sponsor=1"
40
+ "Homepage" = "https://github.com/liblaf/conf"
41
+ "Issue Tracker" = "https://github.com/liblaf/conf/issues"
42
+ "Release Notes" = "https://github.com/liblaf/conf/releases"
43
+ "Source Code" = "https://github.com/liblaf/conf"
44
+
45
+ [dependency-groups]
46
+ dev = []
47
+ build = ["check-wheel-contents>=0.6", "hatch>=1", "twine>=6"]
48
+ docs = ["liblaf-mkdocs-preset>=0.3", "mkdocs>=1,<2"]
49
+ nox = ["liblaf-nox-recipes>=0.1", "nox>=2026"]
50
+ test = [
51
+ "pytest>=9",
52
+ "pytest-cov>=7",
53
+ "pytest-randomly>=4",
54
+ "pytest-xdist>=3",
55
+ { include-group = "nox" }
56
+ ]
57
+
58
+ [build-system]
59
+ requires = ["hatch-vcs", "hatchling"]
60
+ build-backend = "hatchling.build"
61
+
62
+ [tool.check-wheel-contents]
63
+ ignore = ["W002"]
64
+
65
+ [tool.coverage.run]
66
+ branch = true
67
+ source = ["src"]
68
+
69
+ [tool.hatch.build.hooks.vcs]
70
+ version-file = "src/liblaf/conf/_version.py"
71
+
72
+ [tool.hatch.build.targets.sdist]
73
+ only-include = ["src"]
74
+
75
+ [tool.hatch.build.targets.wheel]
76
+ packages = ["src/liblaf"]
77
+
78
+ [tool.hatch.version]
79
+ source = "vcs"
80
+
81
+ [tool.pytest]
82
+ addopts = [
83
+ "--doctest-modules",
84
+ "--import-mode=importlib",
85
+ "--numprocesses=auto",
86
+ "--showlocals"
87
+ ]
88
+ consider_namespace_packages = true
89
+ strict = true
90
+ testpaths = ["benches", "src", "tests"]
91
+
92
+ [tool.ruff]
93
+ extend = ".config/linters/.ruff.toml"
94
+
95
+ [tool.ty.environment]
96
+ python = ".venv" # ty-vscode does not work without this
97
+
98
+ [tool.uv]
99
+ default-groups = "all"
@@ -0,0 +1,4 @@
1
+ import lazy_loader as lazy
2
+
3
+ __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__)
4
+ del lazy
@@ -0,0 +1,45 @@
1
+ from ._config import BaseConfig, ConfigMeta
2
+ from ._field import Converter, Factory, Field
3
+ from ._field_specifiers import (
4
+ field_bool,
5
+ field_date,
6
+ field_datetime,
7
+ field_decimal,
8
+ field_float,
9
+ field_int,
10
+ field_json,
11
+ field_list_str,
12
+ field_path,
13
+ field_str,
14
+ field_time,
15
+ field_timedelta,
16
+ )
17
+ from ._group import Group, group
18
+ from ._var import Var
19
+ from ._version import __commit_id__, __version__, __version_tuple__
20
+
21
+ __all__ = [
22
+ "BaseConfig",
23
+ "ConfigMeta",
24
+ "Converter",
25
+ "Factory",
26
+ "Field",
27
+ "Group",
28
+ "Var",
29
+ "__commit_id__",
30
+ "__version__",
31
+ "__version_tuple__",
32
+ "field_bool",
33
+ "field_date",
34
+ "field_datetime",
35
+ "field_decimal",
36
+ "field_float",
37
+ "field_int",
38
+ "field_json",
39
+ "field_list_str",
40
+ "field_path",
41
+ "field_str",
42
+ "field_time",
43
+ "field_timedelta",
44
+ "group",
45
+ ]
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import types
5
+ from collections.abc import Generator, Mapping
6
+ from typing import TYPE_CHECKING, Any, ClassVar, Self, cast
7
+
8
+ from pydantic import alias_generators
9
+
10
+ from ._field import Field
11
+ from ._group import Group
12
+
13
+ if TYPE_CHECKING:
14
+ from ._var import Var
15
+
16
+
17
+ class ConfigMeta(type):
18
+ def __new__(
19
+ mcs,
20
+ name: str,
21
+ bases: tuple[type, ...],
22
+ namespace: dict[str, Any],
23
+ /,
24
+ **_kwargs: Any,
25
+ ) -> type:
26
+ if "name" not in namespace:
27
+ namespace["name"] = alias_generators.to_snake(name).removesuffix("_config")
28
+ if "env_prefix" not in namespace:
29
+ namespace["env_prefix"] = namespace["name"].upper() + "_"
30
+ cls: type[BaseConfig] = cast(
31
+ "type[BaseConfig]", super().__new__(mcs, name, bases, namespace)
32
+ )
33
+ fields: dict[str, Field[Any]] = {}
34
+ groups: dict[str, Group[Any]] = {}
35
+ for base in reversed(cls.__mro__[1:]):
36
+ fields.update(getattr(base, "_fields", {}))
37
+ groups.update(getattr(base, "_groups", {}))
38
+ for key, value in namespace.items():
39
+ match value:
40
+ case Field():
41
+ groups.pop(key, None)
42
+ fields[key] = value
43
+ case Group():
44
+ fields.pop(key, None)
45
+ groups[key] = value
46
+ case _:
47
+ fields.pop(key, None)
48
+ groups.pop(key, None)
49
+ cls._fields = fields
50
+ cls._groups = groups
51
+ return cls
52
+
53
+ def __call__[T: BaseConfig](cls: type[T], *args, **kwargs) -> T:
54
+ instance: T | None = cls.__dict__.get("_instance")
55
+ if instance is None:
56
+ instance = super().__call__(*args, **kwargs) # ty:ignore[invalid-super-argument]
57
+ cls._instance = instance
58
+ return instance
59
+
60
+
61
+ class BaseConfig(metaclass=ConfigMeta):
62
+ name: ClassVar[str]
63
+ env_prefix: ClassVar[str]
64
+ _fields: ClassVar[dict[str, Field[Any]]]
65
+ _groups: ClassVar[dict[str, Group[Any]]]
66
+ _instance: ClassVar[Self | None] = None
67
+
68
+ def load_env(self) -> None:
69
+ for name in self._fields:
70
+ var: Var[Any] = self._get_field(name)
71
+ var.load_env()
72
+ for name in self._groups:
73
+ group: BaseConfig = self._get_group(name)
74
+ group.load_env()
75
+
76
+ def set(self, changes: Mapping[str, Any] | None = None, /, **kwargs: Any) -> None:
77
+ if changes is not None:
78
+ kwargs.update(changes)
79
+ for name, value in kwargs.items():
80
+ var: BaseConfig | Var[Any] = self._get_field_or_group(name)
81
+ var.set(value)
82
+
83
+ @contextlib.contextmanager
84
+ def override(
85
+ self, changes: Mapping[str, Any] | None = None, /, **kwargs: Any
86
+ ) -> Generator[None]:
87
+ if changes is not None:
88
+ kwargs.update(changes)
89
+ with contextlib.ExitStack() as stack:
90
+ for name, value in kwargs.items():
91
+ var: BaseConfig | Var[Any] = self._get_field_or_group(name)
92
+ stack.enter_context(var.override(value))
93
+ yield
94
+
95
+ def to_dict(self) -> dict[str, Any]:
96
+ result: dict[str, Any] = {}
97
+ for name in self._fields:
98
+ result[name] = self._get_field(name).get()
99
+ for name in self._groups:
100
+ result[name] = self._get_group(name).to_dict()
101
+ return result
102
+
103
+ def to_namespace(self) -> types.SimpleNamespace:
104
+ result: types.SimpleNamespace = types.SimpleNamespace()
105
+ for name in self._fields:
106
+ setattr(result, name, self._get_field(name).get())
107
+ for name in self._groups:
108
+ setattr(result, name, self._get_group(name).to_namespace())
109
+ return result
110
+
111
+ def _get_field(self, name: str) -> Var[Any]:
112
+ return getattr(self, name)
113
+
114
+ def _get_field_or_group(self, name: str) -> BaseConfig | Var[Any]:
115
+ return getattr(self, name)
116
+
117
+ def _get_group(self, name: str) -> BaseConfig:
118
+ return getattr(self, name)
@@ -0,0 +1,43 @@
1
+ import dataclasses
2
+ from typing import Self, overload
3
+
4
+ from . import converters
5
+ from ._sentinel import MISSING, MissingType
6
+ from ._types import ConfigProtocol, Converter, Factory
7
+ from ._var import Var
8
+
9
+
10
+ @dataclasses.dataclass(frozen=True, slots=True, weakref_slot=True)
11
+ class Field[T]:
12
+ env: str | None = None
13
+ default: T | MissingType = MISSING
14
+ factory: Factory[T] | None = None
15
+ converter: Converter[T] = converters.identity
16
+ name: str = dataclasses.field(init=False)
17
+
18
+ @overload
19
+ def __get__(self, instance: None, owner: type | None = None) -> Self: ...
20
+ @overload
21
+ def __get__(self, instance: object, owner: type | None = None) -> Var[T]: ...
22
+ def __get__(
23
+ self, instance: ConfigProtocol | None, owner: type | None = None
24
+ ) -> Self | Var[T]:
25
+ if instance is None:
26
+ return self
27
+ if self.name not in instance.__dict__:
28
+ instance.__dict__[self.name] = self._bind(instance)
29
+ return instance.__dict__[self.name]
30
+
31
+ def __set_name__(self, owner: type, name: str) -> None:
32
+ object.__setattr__(self, "name", name)
33
+
34
+ def _bind(self, instance: ConfigProtocol) -> Var[T]:
35
+ name: str = instance.name + "." + self.name
36
+ env: str = self.env or instance.env_prefix + self.name.upper()
37
+ return Var(
38
+ name,
39
+ default=self.default,
40
+ factory=self.factory,
41
+ env=env,
42
+ converter=self.converter,
43
+ )
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import json
5
+ from decimal import Decimal
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from pydantic_extra_types.pendulum_dt import Date, DateTime, Duration, Time
10
+
11
+ from . import converters
12
+ from ._field import Field
13
+ from ._sentinel import MISSING, MissingType
14
+ from ._types import Converter, Factory
15
+
16
+ if TYPE_CHECKING:
17
+ from _typeshed import StrPath
18
+
19
+
20
+ def field_bool(
21
+ env: str | None = None,
22
+ default: bool | MissingType = MISSING, # noqa: FBT001
23
+ factory: Factory[bool] | None = None,
24
+ converter: Converter[bool] | None = None,
25
+ ) -> Field[bool]:
26
+ if converter is None:
27
+ converter: Converter[bool] = converters.pydantic_type_adapter_validate_strings(
28
+ bool
29
+ )
30
+ return Field(env=env, default=default, factory=factory, converter=converter)
31
+
32
+
33
+ def field_date(
34
+ env: str | None = None,
35
+ default: Any | MissingType = MISSING,
36
+ factory: Factory[Any] | None = None,
37
+ converter: Converter[Any] | None = None,
38
+ ) -> Field[Any]:
39
+ if converter is None:
40
+ converter: Converter[datetime.date] = (
41
+ converters.pydantic_type_adapter_validate_strings(Date)
42
+ )
43
+ return Field(env=env, default=default, factory=factory, converter=converter)
44
+
45
+
46
+ def field_datetime(
47
+ env: str | None = None,
48
+ default: datetime.datetime | MissingType = MISSING,
49
+ factory: Factory[datetime.datetime] | None = None,
50
+ converter: Converter[datetime.datetime] | None = None,
51
+ ) -> Field[datetime.datetime]:
52
+ if converter is None:
53
+ converter: Converter[datetime.datetime] = (
54
+ converters.pydantic_type_adapter_validate_strings(DateTime)
55
+ )
56
+ return Field(env=env, default=default, factory=factory, converter=converter)
57
+
58
+
59
+ def field_decimal(
60
+ env: str | None = None,
61
+ default: Decimal | MissingType = MISSING,
62
+ factory: Factory[Decimal] | None = None,
63
+ converter: Converter[Decimal] | None = None,
64
+ ) -> Field[Decimal]:
65
+ if converter is None:
66
+ converter: Converter[Decimal] = Decimal
67
+ return Field(env=env, default=default, factory=factory, converter=converter)
68
+
69
+
70
+ def field_float(
71
+ env: str | None = None,
72
+ default: float | MissingType = MISSING,
73
+ factory: Factory[float] | None = None,
74
+ converter: Converter[float] | None = None,
75
+ ) -> Field[float]:
76
+ if converter is None:
77
+ converter: Converter[float] = float
78
+ return Field(env=env, default=default, factory=factory, converter=converter)
79
+
80
+
81
+ def field_int(
82
+ env: str | None = None,
83
+ default: int | MissingType = MISSING,
84
+ factory: Factory[int] | None = None,
85
+ converter: Converter[int] | None = None,
86
+ ) -> Field[int]:
87
+ if converter is None:
88
+ converter: Converter[int] = int
89
+ return Field(env=env, default=default, factory=factory, converter=converter)
90
+
91
+
92
+ def field_json(
93
+ env: str | None = None,
94
+ default: Any | MissingType = MISSING,
95
+ factory: Factory[Any] | None = None,
96
+ converter: Converter[Any] | None = None,
97
+ ) -> Field[Any]:
98
+ if converter is None:
99
+ converter: Converter[Any] = json.loads
100
+ return Field(env=env, default=default, factory=factory, converter=converter)
101
+
102
+
103
+ def field_list_str(
104
+ env: str | None = None,
105
+ default: list[str] | MissingType = MISSING,
106
+ factory: Factory[list[str]] | None = None,
107
+ converter: Converter[list[str]] | None = None,
108
+ delimiter: str = ",",
109
+ ) -> Field[list[str]]:
110
+ if converter is None:
111
+
112
+ def converter(value: str) -> list[str]:
113
+ return [item.strip() for item in value.split(delimiter)]
114
+
115
+ return Field(env=env, default=default, factory=factory, converter=converter)
116
+
117
+
118
+ def field_path(
119
+ env: str | None = None,
120
+ default: StrPath | MissingType = MISSING,
121
+ factory: Factory[StrPath] | None = None,
122
+ converter: Converter[Path] | None = None,
123
+ ) -> Field[Path]:
124
+ if converter is None:
125
+ converter: Converter[Path] = Path
126
+ if default is not MISSING:
127
+ default = Path(default)
128
+ if factory is not None:
129
+ wrapped: Factory[StrPath] = factory
130
+
131
+ def factory() -> Path:
132
+ return Path(wrapped())
133
+
134
+ return Field(env=env, default=default, factory=factory, converter=converter)
135
+
136
+
137
+ def field_str(
138
+ env: str | None = None,
139
+ default: str | MissingType = MISSING,
140
+ factory: Factory[str] | None = None,
141
+ converter: Converter[str] | None = None,
142
+ ) -> Field[str]:
143
+ if converter is None:
144
+ converter: Converter[str] = converters.identity
145
+ return Field(env=env, default=default, factory=factory, converter=converter)
146
+
147
+
148
+ def field_time(
149
+ env: str | None = None,
150
+ default: datetime.time | MissingType = MISSING,
151
+ factory: Factory[datetime.time] | None = None,
152
+ converter: Converter[datetime.time] | None = None,
153
+ ) -> Field[datetime.time]:
154
+ if converter is None:
155
+ converter: Converter[datetime.time] = (
156
+ converters.pydantic_type_adapter_validate_strings(Time)
157
+ )
158
+ return Field(env=env, default=default, factory=factory, converter=converter)
159
+
160
+
161
+ def field_timedelta(
162
+ env: str | None = None,
163
+ default: datetime.timedelta | MissingType = MISSING,
164
+ factory: Factory[datetime.timedelta] | None = None,
165
+ converter: Converter[datetime.timedelta] | None = None,
166
+ ) -> Field[datetime.timedelta]:
167
+ if converter is None:
168
+ converter: Converter[datetime.timedelta] = (
169
+ converters.pydantic_type_adapter_validate_strings(Duration)
170
+ )
171
+ return Field(env=env, default=default, factory=factory, converter=converter)
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ from collections.abc import Callable
5
+ from typing import Self, overload
6
+
7
+
8
+ @dataclasses.dataclass(frozen=True, slots=True, weakref_slot=True)
9
+ class Group[T]:
10
+ factory: Callable[[], T]
11
+ name: str = dataclasses.field(init=False)
12
+
13
+ @overload
14
+ def __get__(self, instance: None, owner: type | None = None) -> Self: ...
15
+ @overload
16
+ def __get__(self, instance: object, owner: type | None = None) -> T: ...
17
+ def __get__(self, instance: object | None, owner: type | None = None) -> Self | T:
18
+ if instance is None:
19
+ return self
20
+ if self.name not in instance.__dict__:
21
+ instance.__dict__[self.name] = self.factory()
22
+ return instance.__dict__[self.name]
23
+
24
+ def __set_name__(self, owner: type, name: str) -> None:
25
+ object.__setattr__(self, "name", name)
26
+
27
+
28
+ @overload
29
+ def group[T](factory: type[T]) -> Group[T]: ...
30
+ @overload
31
+ def group[T](factory: Callable[[], T]) -> Group[T]: ...
32
+ def group[T](factory: Callable[[], T]) -> Group[T]:
33
+ return Group(factory)
@@ -0,0 +1,8 @@
1
+ import enum
2
+
3
+
4
+ class MissingType(enum.Enum):
5
+ MISSING = enum.auto()
6
+
7
+
8
+ MISSING: MissingType = MissingType.MISSING
@@ -0,0 +1,11 @@
1
+ from collections.abc import Callable
2
+ from typing import ClassVar, Protocol
3
+
4
+
5
+ class ConfigProtocol(Protocol):
6
+ name: ClassVar[str]
7
+ env_prefix: ClassVar[str]
8
+
9
+
10
+ type Converter[T] = Callable[[str], T]
11
+ type Factory[T] = Callable[[], T]
@@ -0,0 +1,86 @@
1
+ import contextlib
2
+ import contextvars
3
+ import dataclasses
4
+ import os
5
+ from collections.abc import Generator
6
+ from typing import Any, cast, overload
7
+
8
+ from . import converters
9
+ from ._sentinel import MISSING, MissingType
10
+ from ._types import Converter, Factory
11
+
12
+
13
+ @dataclasses.dataclass(init=False, frozen=True, slots=True, weakref_slot=True)
14
+ class Var[T]:
15
+ env: str | None
16
+ converter: Converter[T]
17
+ _var: contextvars.ContextVar[T]
18
+
19
+ def __init__(
20
+ self,
21
+ name: str,
22
+ default: T | MissingType = MISSING,
23
+ factory: Factory[T] | None = None,
24
+ env: str | None = None,
25
+ converter: Converter[T] | None = None,
26
+ ) -> None:
27
+ if converter is None:
28
+ converter: Converter[T] = converters.identity
29
+ if env is not None:
30
+ value: str | None = os.getenv(env)
31
+ if value is not None:
32
+ default: T = cast("T", converter(value))
33
+ if default is MISSING and factory is not None:
34
+ default: T = factory()
35
+ if default is MISSING:
36
+ var: contextvars.ContextVar[T] = contextvars.ContextVar(name)
37
+ else:
38
+ var: contextvars.ContextVar[T] = contextvars.ContextVar(
39
+ name, default=default
40
+ )
41
+ object.__setattr__(self, "_var", var)
42
+ object.__setattr__(self, "env", env)
43
+ object.__setattr__(self, "converter", converter)
44
+
45
+ def __hash__(self) -> int:
46
+ return hash(self._var)
47
+
48
+ @property
49
+ def name(self) -> str:
50
+ return self._var.name
51
+
52
+ @overload
53
+ def get(self) -> T: ...
54
+ @overload
55
+ def get(self, default: T, /) -> T: ...
56
+ @overload
57
+ def get[D](self, default: D, /) -> D | T: ...
58
+ def get(self, default: Any = MISSING) -> T:
59
+ if default is MISSING:
60
+ return self._var.get()
61
+ return self._var.get(default)
62
+
63
+ def set(self, value: T) -> contextvars.Token[T]:
64
+ return self._var.set(value)
65
+
66
+ def reset(self, token: contextvars.Token[T]) -> None:
67
+ self._var.reset(token)
68
+
69
+ def load_env(self) -> None:
70
+ if self.env is None:
71
+ return
72
+ value: str | None = os.getenv(self.env)
73
+ if value is None:
74
+ return
75
+ if self.converter is None:
76
+ self.set(cast("T", value))
77
+ else:
78
+ self.set(self.converter(value))
79
+
80
+ @contextlib.contextmanager
81
+ def override(self, value: T) -> Generator[None]:
82
+ token: contextvars.Token[T] = self._var.set(value)
83
+ try:
84
+ yield
85
+ finally:
86
+ self._var.reset(token)
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.1.0'
32
+ __version_tuple__ = version_tuple = (0, 1, 0)
33
+
34
+ __commit_id__ = commit_id = None
@@ -0,0 +1,8 @@
1
+ # ref: <https://github.com/ofek/hatch-vcs>
2
+ # ref: <https://github.com/pypa/setuptools-scm/blob/58ed7d5dec0e3bc80190f73543cf4d8a6876ba8e/src/setuptools_scm/_integration/dump_version.py#L16-L51>
3
+ __commit_id__: str | None
4
+ __version__: str
5
+ __version_tuple__: tuple[int | str, ...]
6
+ commit_id: str | None
7
+ version_tuple: tuple[int | str, ...]
8
+ version: str
@@ -0,0 +1,4 @@
1
+ import lazy_loader as lazy
2
+
3
+ __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__)
4
+ del lazy
@@ -0,0 +1,19 @@
1
+ from ._misc import identity
2
+ from ._pydantic import (
3
+ pydantic_model_validate,
4
+ pydantic_model_validate_json,
5
+ pydantic_model_validate_strings,
6
+ pydantic_type_adapter_validate_json,
7
+ pydantic_type_adapter_validate_python,
8
+ pydantic_type_adapter_validate_strings,
9
+ )
10
+
11
+ __all__ = [
12
+ "identity",
13
+ "pydantic_model_validate",
14
+ "pydantic_model_validate_json",
15
+ "pydantic_model_validate_strings",
16
+ "pydantic_type_adapter_validate_json",
17
+ "pydantic_type_adapter_validate_python",
18
+ "pydantic_type_adapter_validate_strings",
19
+ ]
@@ -0,0 +1,2 @@
1
+ def identity[T](x: T) -> T:
2
+ return x
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import pydantic
6
+
7
+ if TYPE_CHECKING:
8
+ from liblaf.conf._field import Converter
9
+
10
+
11
+ def pydantic_model_validate[T: pydantic.BaseModel](model: type[T]) -> Converter[T]:
12
+ return model.model_validate
13
+
14
+
15
+ def pydantic_model_validate_json[T: pydantic.BaseModel](model: type[T]) -> Converter[T]:
16
+ return model.model_validate_json
17
+
18
+
19
+ def pydantic_model_validate_strings[T: pydantic.BaseModel](
20
+ model: type[T],
21
+ ) -> Converter[T]:
22
+ return model.model_validate_strings
23
+
24
+
25
+ def pydantic_type_adapter_validate_json[T](type_: type[T]) -> Converter[T]:
26
+ return pydantic.TypeAdapter(type_).validate_json
27
+
28
+
29
+ def pydantic_type_adapter_validate_python[T](type_: type[T]) -> Converter[T]:
30
+ return pydantic.TypeAdapter(type_).validate_python
31
+
32
+
33
+ def pydantic_type_adapter_validate_strings[T](type_: type[T]) -> Converter[T]:
34
+ return pydantic.TypeAdapter(type_).validate_strings
File without changes