rewire 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.
rewire-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 rewirepy
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.
rewire-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.1
2
+ Name: rewire
3
+ Version: 0.1.0
4
+ Summary: A Streamlined Type-Based Dependency Injection Framework
5
+ Author-email: Ivan Vozhakov <gou177@bk.ru>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 rewirepy
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/rewirepy/example-fastapi
29
+ Keywords: dependency injection,configuration managment
30
+ Classifier: License :: OSI Approved :: MIT License
31
+ Classifier: Programming Language :: Python
32
+ Classifier: Programming Language :: Python :: 3
33
+ Requires-Python: >=3.12
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENSE
36
+ Requires-Dist: pydantic>=2.0.0
37
+ Requires-Dist: anyio>=4.0.0
38
+ Requires-Dist: PyYAML>=6.0.1
39
+ Requires-Dist: loguru<=1.0.0
40
+ Provides-Extra: dev
41
+ Requires-Dist: black; extra == "dev"
42
+ Requires-Dist: pytest; extra == "dev"
43
+ Requires-Dist: trio; extra == "dev"
44
+ Requires-Dist: pytest-anyio; extra == "dev"
45
+ Requires-Dist: graphviz; extra == "dev"
46
+
47
+
48
+ # Rewire: A Streamlined Type-Based Dependency Injection Framework for Focused Development
49
+
50
+ Rewire is a user-friendly framework dedicated to type-based dependency injection, allowing developers to focus on writing code rather than dealing with boilerplate.
51
+
52
+ ## Key Features and Benefits
53
+
54
+ ### Dependency Injection
55
+
56
+ Employ Rewire's advanced type-based dependency injection system to automatically manage the instantiation, configuration, and resolution of components in your application. This lets you concentrate on
57
+ developing functionality without being bogged down by dependency management.
58
+
59
+ ### Configuration Injection (YAML & Python Functions)
60
+
61
+ Easily load configurations from a YAML file using Rewire's versatile configuration injection mechanism, which supports Python function extensions. Set up complex applications with ease and consistency
62
+ while maintaining flexibility.
63
+
64
+ ### Automatic File Import
65
+
66
+ Automatically import required files based on defined configurations within your Rewire-powered application. Keep your project organized by trusting Rewire to handle file imports, promoting code
67
+ reusability and minimizing potential errors.
68
+
69
+ ### Lifecycle Manager
70
+
71
+ Easily manage the lifecycles of components with Rewire's robust start/stop manager. Control the initialization and disposal of resources efficiently, ensuring optimal resource usage.
72
+
73
+ ### Extensibility & Hackability
74
+
75
+ Rewire is designed to be both extensible and hackable, enabling developers to tailor the framework to their specific needs. Customize components, add new features, or integrate Rewire with other
76
+ libraries as desired, making it a powerful choice for any project.
rewire-0.1.0/README.md ADDED
@@ -0,0 +1,30 @@
1
+
2
+ # Rewire: A Streamlined Type-Based Dependency Injection Framework for Focused Development
3
+
4
+ Rewire is a user-friendly framework dedicated to type-based dependency injection, allowing developers to focus on writing code rather than dealing with boilerplate.
5
+
6
+ ## Key Features and Benefits
7
+
8
+ ### Dependency Injection
9
+
10
+ Employ Rewire's advanced type-based dependency injection system to automatically manage the instantiation, configuration, and resolution of components in your application. This lets you concentrate on
11
+ developing functionality without being bogged down by dependency management.
12
+
13
+ ### Configuration Injection (YAML & Python Functions)
14
+
15
+ Easily load configurations from a YAML file using Rewire's versatile configuration injection mechanism, which supports Python function extensions. Set up complex applications with ease and consistency
16
+ while maintaining flexibility.
17
+
18
+ ### Automatic File Import
19
+
20
+ Automatically import required files based on defined configurations within your Rewire-powered application. Keep your project organized by trusting Rewire to handle file imports, promoting code
21
+ reusability and minimizing potential errors.
22
+
23
+ ### Lifecycle Manager
24
+
25
+ Easily manage the lifecycles of components with Rewire's robust start/stop manager. Control the initialization and disposal of resources efficiently, ensuring optimal resource usage.
26
+
27
+ ### Extensibility & Hackability
28
+
29
+ Rewire is designed to be both extensible and hackable, enabling developers to tailor the framework to their specific needs. Customize components, add new features, or integrate Rewire with other
30
+ libraries as desired, making it a powerful choice for any project.
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "rewire"
7
+ version = "0.1.0"
8
+ description = "A Streamlined Type-Based Dependency Injection Framework"
9
+ readme = "README.md"
10
+ authors = [{ name = "Ivan Vozhakov", email = "gou177@bk.ru" }]
11
+ license = { file = "LICENSE" }
12
+ classifiers = [
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python",
15
+ "Programming Language :: Python :: 3",
16
+ ]
17
+ keywords = ["dependency injection", "configuration managment"]
18
+ dependencies = [
19
+ "pydantic >= 2.0.0",
20
+ "anyio >= 4.0.0",
21
+ "PyYAML >= 6.0.1",
22
+ "loguru <= 1.0.0",
23
+ ]
24
+ requires-python = ">=3.12"
25
+
26
+ [project.optional-dependencies]
27
+ dev = ["black", "pytest", "trio", "pytest-anyio", "graphviz"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/rewirepy/example-fastapi"
@@ -0,0 +1 @@
1
+ from . import context, dependencies, plugins, config, log, lifecycle
@@ -0,0 +1,10 @@
1
+ from typing import Callable
2
+
3
+
4
+ class classproperty[T](property):
5
+ def __init__(self, cb: Callable[..., T]) -> None:
6
+ self.cb = cb
7
+ super().__init__()
8
+
9
+ def __get__(self, _, owner) -> T:
10
+ return self.cb.__func__(owner) # type: ignore
@@ -0,0 +1,417 @@
1
+ from contextvars import ContextVar
2
+ from functools import lru_cache
3
+ import json
4
+ from os import getenv
5
+ import os
6
+ from pathlib import Path
7
+ from typing import (
8
+ Annotated,
9
+ Any,
10
+ Callable,
11
+ ClassVar,
12
+ Dict,
13
+ Literal,
14
+ Optional,
15
+ Self,
16
+ Type,
17
+ cast,
18
+ overload,
19
+ )
20
+ from loguru import logger
21
+ from pydantic import BaseModel, Field, TypeAdapter
22
+ import yaml
23
+ from rewire.classproperty import classproperty
24
+ from rewire.context import use_context_value
25
+ import inspect
26
+ from rewire.dependencies import Dependency, TypeRef
27
+ from rewire.space import Module
28
+
29
+ from rewire.store import SimpleStore
30
+
31
+ CONFIG_FILE = getenv("CONFIG_FILE", "./config.yaml")
32
+
33
+ UNSET = object()
34
+
35
+
36
+ cwdContext = ContextVar("rewire.config.cwdConfig", default="./")
37
+ fileContext = ContextVar("rewire.config.fileContext", default=CONFIG_FILE)
38
+ rootContext = ContextVar("rewire.config.rootContext")
39
+
40
+
41
+ class ConfigLoader(yaml.SafeLoader):
42
+ pass
43
+
44
+
45
+ class EnvRequired(Exception):
46
+ pass
47
+
48
+
49
+ class PyCode(BaseModel):
50
+ code: str
51
+ cwd: str = Field(default_factory=cwdContext.get)
52
+ file: str = Field(default_factory=fileContext.get)
53
+
54
+ def execute(self):
55
+ return eval(
56
+ self.code,
57
+ {"this": rootContext.get(), "self": rootContext.get()},
58
+ self.functions(),
59
+ )
60
+
61
+ def functions(self):
62
+ return {"include": self.include, "getenv": getenv}
63
+
64
+ def include(self, file: str):
65
+ with (
66
+ use_context_value(cwdContext, self.cwd),
67
+ use_context_value(fileContext, self.file),
68
+ ):
69
+ return load_yaml(file)
70
+
71
+
72
+ class PyExecCode(PyCode):
73
+ def execute(self):
74
+ container = SimpleStore[Callable]()
75
+
76
+ exec(
77
+ "def __pyexec():\n"
78
+ + "\n".join(f" {x}" for x in self.code.splitlines())
79
+ + "\n__container.set(__pyexec)",
80
+ {
81
+ "this": rootContext.get(),
82
+ "self": rootContext.get(),
83
+ "__container": container,
84
+ },
85
+ self.functions(),
86
+ )
87
+ fn = container.get()
88
+ assert fn
89
+ return fn()
90
+
91
+
92
+ class EvalDict(dict):
93
+ __pydantic_validator__ = None
94
+
95
+ def __getitem__(self, __k):
96
+ if __k not in self:
97
+ raise AttributeError(__k)
98
+ value = super().__getitem__(__k)
99
+
100
+ if isinstance(value, PyCode):
101
+ return value.execute()
102
+
103
+ if isinstance(value, dict):
104
+ return type(self)(value)
105
+
106
+ return value
107
+
108
+ def __getattribute__(self, __name: str) -> Any:
109
+ try:
110
+ return super().__getattribute__(__name)
111
+ except AttributeError:
112
+ return self.__getitem__(__name) # type: ignore
113
+
114
+
115
+ def load_yaml_env(loader, node: yaml.ScalarNode):
116
+ value = getenv(*node.value.split(":", 1))
117
+ if value is None:
118
+ raise EnvRequired(f'env variable {node.value.split(":", 1)[0]!r} required')
119
+ return value
120
+
121
+
122
+ def load_yaml_include(loader, node: yaml.ScalarNode):
123
+ return load_yaml(node.value)
124
+
125
+
126
+ def load_yaml_py(load, node: yaml.ScalarNode):
127
+ return PyCode(code=node.value)
128
+
129
+
130
+ def load_yaml_pyexec(load, node: yaml.ScalarNode):
131
+ return PyExecCode(code=node.value)
132
+
133
+
134
+ def load_yaml_yaml(load, node: yaml.ScalarNode):
135
+ return prepare_yaml(yaml.load(node.value, ConfigLoader))
136
+
137
+
138
+ ConfigLoader.add_constructor("!env", load_yaml_env)
139
+ ConfigLoader.add_constructor("!include", load_yaml_include)
140
+ ConfigLoader.add_constructor("!py", load_yaml_py)
141
+ ConfigLoader.add_constructor("!pyexec", load_yaml_pyexec)
142
+ ConfigLoader.add_constructor("!yaml", load_yaml_yaml)
143
+
144
+
145
+ @overload
146
+ def merge[K, T, KO, TO](
147
+ source: dict[K, T], overlay: dict[KO, TO]
148
+ ) -> dict[K | KO, T | TO]:
149
+ ...
150
+
151
+
152
+ @overload
153
+ def merge[T](source: Any, overlay: T) -> T:
154
+ ...
155
+
156
+
157
+ def merge(source: dict | list | Any, overlay: dict | list | Any):
158
+ if isinstance(source, dict | list):
159
+ source = source.copy()
160
+ if isinstance(source, dict) and isinstance(overlay, dict):
161
+ for key, value in overlay.items():
162
+ source[key] = merge(source.get(key, None), value)
163
+ return source
164
+ return overlay
165
+
166
+
167
+ def render_py(value, root=UNSET):
168
+ if root is UNSET:
169
+ root = value
170
+ if isinstance(value, dict):
171
+ new_value = {k: render_py(v, root=root) for k, v in value.items()}
172
+ value.update(new_value)
173
+ return EvalDict(value)
174
+ if isinstance(value, list):
175
+ new_value = [render_py(v, root=root) for v in value]
176
+ value.clear()
177
+ value.extend(new_value)
178
+ return value
179
+ if isinstance(value, PyCode):
180
+ return render_py(value.execute(), root=root)
181
+ return value
182
+
183
+
184
+ def load_yaml(file_: str | Path):
185
+ cwd = cwdContext.get().removesuffix("/") + "/"
186
+ file_ = os.path.join(cwd, file_)
187
+ with (
188
+ open(file_) as f,
189
+ use_context_value(fileContext, file_),
190
+ use_context_value(cwdContext, os.path.dirname(file_)),
191
+ ):
192
+ value = yaml.load(f, ConfigLoader)
193
+ return prepare_yaml(value)
194
+
195
+
196
+ def prepare_yaml(value):
197
+ with use_context_value(rootContext, None):
198
+ return EvalDict({"d": value}).d
199
+
200
+
201
+ def set_by_key(key, value, data):
202
+ if key:
203
+ for k in key.split(".")[:-1]:
204
+ data = data.setdefault(k, {})
205
+ data[key.split(".")[-1]] = value
206
+ else:
207
+ data.update(value)
208
+
209
+
210
+ def get_by_key(key, data):
211
+ if key is None:
212
+ return data
213
+ if "." in key:
214
+ key, next_key = key.split(".", 1)
215
+ return get_by_key(next_key, data.get(key, {}))
216
+ return data.get(key, {})
217
+
218
+
219
+ def merge_env(data: Dict): # /NOSONAR
220
+ remap_config = getenv("CONFIG_REWIRE_ENV_REMAP", {})
221
+ if isinstance(remap_config, str):
222
+ remap_config = json.loads(remap_config)
223
+ assert isinstance(remap_config, dict)
224
+
225
+ for key, value in os.environ.items():
226
+ remapped = key in remap_config
227
+ if remapped:
228
+ key = remap_config[key]
229
+
230
+ if not key.startswith("CONFIG_") and not remapped:
231
+ continue
232
+
233
+ if not remapped:
234
+ key = key.removeprefix("CONFIG_")
235
+ key = key.replace("_", ".").replace("..", "_")
236
+
237
+ if key.endswith("_.JSON") or remapped and key.endswith(":json"):
238
+ key = key.removesuffix("_.JSON").removesuffix(":json")
239
+ value = json.loads(value)
240
+ elif key.endswith("_.YAML") or remapped and key.endswith(":yaml"):
241
+ key = key.removesuffix("_.YAML").removesuffix(":yaml")
242
+ value = yaml.load(value, ConfigLoader)
243
+ value = prepare_yaml(value)
244
+
245
+ set_by_key(key, value, data)
246
+ return data
247
+
248
+
249
+ @lru_cache()
250
+ def parse_file(file: str | Path, silent: bool = False):
251
+ try:
252
+ raw_config = TypeAdapter(Dict[str, Any]).validate_python(load_yaml(file) or {})
253
+ except FileNotFoundError as e:
254
+ if not silent:
255
+ logger.error(e)
256
+ raw_config = {}
257
+
258
+ raw_config = merge_env(raw_config)
259
+
260
+ with use_context_value(rootContext, raw_config):
261
+ raw_config = EvalDict({"d": raw_config}).d
262
+
263
+ with use_context_value(rootContext, raw_config):
264
+ return render_py(raw_config)
265
+
266
+
267
+ def extract_config_by_path(path: str, model: BaseModel):
268
+ config = model
269
+
270
+ for part in path.split("."):
271
+ config = getattr(config, part, {})
272
+
273
+ return config
274
+
275
+
276
+ def gen_config_model[TBM: BaseModel](path: str, model: Type[TBM], name: str = "Config"):
277
+ Model = model # /NOSONAR
278
+
279
+ field, _, next_ = path.partition(".")
280
+ if next_:
281
+ Model = gen_config_model(next_, model, f"{field.capitalize()}{name}")
282
+
283
+ store: SimpleStore[TBM] = SimpleStore()
284
+ no_required_args = True
285
+
286
+ for param in inspect.signature(Model).parameters.values(): # type: ignore
287
+ if param.default is inspect._empty:
288
+ no_required_args = False
289
+
290
+ exec(
291
+ f"field = Field(default_factory=Model) if noRequiredArgs else Field(...)\n"
292
+ f"class {name}(BaseModel):\n"
293
+ f" {field}: Model = field\n"
294
+ f"store.set({name})\n",
295
+ {
296
+ "store": store,
297
+ "Model": Model,
298
+ "BaseModel": BaseModel,
299
+ "Field": Field,
300
+ "noRequiredArgs": no_required_args,
301
+ },
302
+ )
303
+
304
+ value = store.get()
305
+ if not value:
306
+ raise RuntimeError("evaluation failed")
307
+
308
+ return value
309
+
310
+
311
+ class ConfigModule(Module):
312
+ config: dict = {}
313
+ config_file: str = CONFIG_FILE
314
+
315
+ def init(self):
316
+ self.config = cast(dict, parse_file(self.config_file)) # type: ignore
317
+
318
+ def patch(self, override: dict):
319
+ self.config = merge(self.config, override)
320
+
321
+
322
+ class EnvironmentModule(Module):
323
+ env: Literal["production", "dev"] = "production"
324
+
325
+ def init(self):
326
+ config = ConfigModule.get(None)
327
+ if (
328
+ config is not None
329
+ and config.config.get("rewire", {}).get("env", getenv("ENV", None)) == "dev"
330
+ ):
331
+ self.env = "dev"
332
+
333
+ @classmethod
334
+ def current_env(cls):
335
+ self = EnvironmentModule.get(None)
336
+ return "dev" if self is None else self.env
337
+
338
+
339
+ @overload
340
+ def config[TBM: BaseModel](
341
+ model: Type[TBM], *, path: Optional[str] = ".", fallback: Any = None
342
+ ) -> TBM:
343
+ ...
344
+
345
+
346
+ @overload
347
+ def config[TBM: BaseModel](
348
+ *, path: Optional[str] = ".", fallback: Any = None
349
+ ) -> Callable[[Type[TBM]], TBM]:
350
+ ...
351
+
352
+
353
+ def config(model: Any = UNSET, *, path: Optional[str] = ".", fallback: Any = None):
354
+ """fills base model from file fields"""
355
+
356
+ def wrapper[TBM: BaseModel](config: Type[TBM]) -> TBM:
357
+ config_path = update_path(config.__module__, path or ".")
358
+
359
+ data = gen_config_model(config_path, config)
360
+ try:
361
+ raw_config = ConfigModule.get().config
362
+
363
+ return cast(
364
+ config,
365
+ extract_config_by_path(config_path, data.model_validate(raw_config)),
366
+ )
367
+ except LookupError:
368
+ if fallback is None:
369
+ raise
370
+ return config.model_validate(fallback)
371
+
372
+ if model is not UNSET:
373
+ return wrapper(model)
374
+
375
+ return wrapper
376
+
377
+
378
+ def update_path(config_path: str, post_path: str):
379
+ level = 0
380
+
381
+ while post_path.startswith("."):
382
+ level -= 1
383
+ post_path = post_path[1:]
384
+
385
+ if level == 0:
386
+ return post_path
387
+
388
+ for _ in range(1, -level):
389
+ config_path = ".".join(config_path.split(".")[:-1])
390
+
391
+ if post_path:
392
+ config_path = f"{config_path}.{post_path}".strip(".")
393
+ return config_path
394
+
395
+
396
+ class ConfigDependency(BaseModel):
397
+ _dependency: ClassVar[Dependency[Self] | None] = None
398
+ __location__: ClassVar[str] = "."
399
+ __fallback__: ClassVar[dict | None] = None
400
+
401
+ @classproperty
402
+ @classmethod
403
+ def dependency(cls) -> Dependency[Self]:
404
+ if cls._dependency is None:
405
+
406
+ async def resolve():
407
+ return config(cls, path=cls.__location__, fallback=cls.__fallback__)
408
+
409
+ cls._dependency = Dependency(
410
+ cb=resolve, type=cls, label=f"Config ({cls.__module__}.{cls.__name__})"
411
+ )
412
+ return cls._dependency
413
+
414
+ @classproperty
415
+ @classmethod
416
+ def Value(cls) -> Type[Self]:
417
+ return Annotated[cls, TypeRef(type=cls)]
@@ -0,0 +1,79 @@
1
+ from contextlib import contextmanager
2
+ from contextvars import ContextVar
3
+ from typing import Type, overload
4
+ from uuid import uuid4
5
+ from pydantic import BaseModel, PrivateAttr
6
+
7
+ UNSET = object()
8
+
9
+
10
+ class Context[T](BaseModel):
11
+ _ctx: ContextVar[T] | None = PrivateAttr(None)
12
+ name: str | None = None
13
+
14
+ @property
15
+ def ctx(self):
16
+ if self._ctx is not None:
17
+ return self._ctx
18
+ self._ctx = ContextVar(self.name or str(uuid4()))
19
+ return self._ctx
20
+
21
+ @contextmanager
22
+ def use(self: "ContextVar[T] | Context[T]", value: T):
23
+ if isinstance(self, Context):
24
+ self = self.ctx
25
+
26
+ token = self.set(value)
27
+ try:
28
+ yield value
29
+ finally:
30
+ self.reset(token)
31
+
32
+ @overload
33
+ def get[D](self, default: D) -> T | D:
34
+ ...
35
+
36
+ @overload
37
+ def get(self) -> T:
38
+ ...
39
+
40
+ def get(self, default=UNSET):
41
+ if default is UNSET:
42
+ return self.ctx.get()
43
+ return self.ctx.get(default)
44
+
45
+
46
+ def use_context_value[T](self: ContextVar[T] | Context[T], value: T):
47
+ return Context.use(self, value)
48
+
49
+
50
+ class BoundCtx[T](Context[T]):
51
+ _value: T = PrivateAttr()
52
+
53
+ def use(self, value: T | None = None):
54
+ return super().use(value or self._value)
55
+
56
+
57
+ class CTX(property):
58
+ _context: Context | None = None
59
+
60
+ @overload
61
+ def __get__[T](self, __instance: None, __owner: Type[T]) -> Context[T]:
62
+ ...
63
+
64
+ @overload
65
+ def __get__[T](self, __instance: T, __owner: Type[T]) -> BoundCtx[T]:
66
+ ...
67
+
68
+ def __get__[T](
69
+ self, __instance: T | None, __owner: Type[T]
70
+ ) -> Context[T] | BoundCtx[T]:
71
+ if self._context is None:
72
+ self._context = Context()
73
+ if __instance is not None:
74
+ ctx = BoundCtx()
75
+ ctx._ctx = self._context.ctx
76
+ ctx._value = __instance
77
+ ctx.name = self._context.name
78
+ return ctx
79
+ return self._context