raceway 0.2__tar.gz → 0.2.2__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.
- raceway-0.2.2/PKG-INFO +133 -0
- raceway-0.2.2/README.rst +118 -0
- {raceway-0.2 → raceway-0.2.2}/pyproject.toml +37 -2
- raceway-0.2.2/src/raceway/__version__.py +1 -0
- raceway-0.2.2/src/raceway/callbacks.py +103 -0
- raceway-0.2.2/src/raceway/container.py +149 -0
- raceway-0.2.2/src/raceway/extractor.py +163 -0
- raceway-0.2.2/src/raceway/injector.py +58 -0
- raceway-0.2.2/src/raceway/planner.py +105 -0
- raceway-0.2.2/src/raceway/protocols.py +89 -0
- raceway-0.2.2/src/raceway/registration.py +30 -0
- raceway-0.2.2/src/raceway/registry.py +19 -0
- raceway-0.2.2/src/raceway/rules.py +10 -0
- raceway-0.2.2/src/raceway/starter.py +30 -0
- raceway-0.2.2/tests/test_callbacks.py +92 -0
- raceway-0.2.2/tests/test_container.py +103 -0
- raceway-0.2.2/tests/test_extractor.py +75 -0
- raceway-0.2.2/tests/test_free.py +141 -0
- raceway-0.2.2/tests/test_injector.py +99 -0
- raceway-0.2.2/tests/test_planner.py +202 -0
- {raceway-0.2 → raceway-0.2.2}/tests/test_simple.py +20 -23
- raceway-0.2/PKG-INFO +0 -10
- raceway-0.2/src/raceway/__version__.py +0 -1
- raceway-0.2/src/raceway/container.py +0 -104
- raceway-0.2/src/raceway/planner.py +0 -88
- raceway-0.2/src/raceway/protocols.py +0 -43
- raceway-0.2/src/raceway/registration.py +0 -23
- raceway-0.2/src/raceway/registry.py +0 -15
- raceway-0.2/src/raceway/starter.py +0 -30
- raceway-0.2/tests/test_cycles.py +0 -150
- raceway-0.2/tests/test_free.py +0 -124
- {raceway-0.2 → raceway-0.2.2}/.gitignore +0 -0
- {raceway-0.2 → raceway-0.2.2}/LICENSE +0 -0
- {raceway-0.2 → raceway-0.2.2}/src/raceway/__init__.py +0 -0
- {raceway-0.2 → raceway-0.2.2}/src/raceway/exc.py +0 -0
raceway-0.2.2/PKG-INFO
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: raceway
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Simple dependency injection system.
|
|
5
|
+
Project-URL: GitHub, https://github.com/ianjosephwilson/raceway
|
|
6
|
+
Author-email: Ian Wilson <ian@gardentheory.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.14
|
|
12
|
+
Provides-Extra: venusian
|
|
13
|
+
Requires-Dist: venusian; extra == 'venusian'
|
|
14
|
+
Description-Content-Type: text/x-rst
|
|
15
|
+
|
|
16
|
+
raceway
|
|
17
|
+
#########
|
|
18
|
+
|
|
19
|
+
Minimal dependency injection, DI, system ideal for smaller systems that still need DI.
|
|
20
|
+
|
|
21
|
+
Overview
|
|
22
|
+
--------
|
|
23
|
+
|
|
24
|
+
- Defining: Define protocols and implementations.
|
|
25
|
+
- Loading: Load registrations extracted from implementations into planner.
|
|
26
|
+
- Starting: Run startup to create container from planner.
|
|
27
|
+
- Wrapping: Use injector to wrap task handlers to prepare for injection.
|
|
28
|
+
- Running: Create task for each "runner" and use it with the container to execute each injection enabled task handler.
|
|
29
|
+
|
|
30
|
+
Example
|
|
31
|
+
-------
|
|
32
|
+
|
|
33
|
+
This library is meant to be used with an external framework but this example
|
|
34
|
+
uses python's stdlib threading to demonstrate how the pieces fit together.
|
|
35
|
+
|
|
36
|
+
.. code-block:: python
|
|
37
|
+
|
|
38
|
+
import threading
|
|
39
|
+
import time
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
from typing import Protocol
|
|
42
|
+
from random import random
|
|
43
|
+
|
|
44
|
+
from raceway.extractor import configure_extractor
|
|
45
|
+
from raceway.registration import Registration
|
|
46
|
+
from raceway.planner import configure_planner
|
|
47
|
+
from raceway.starter import startup
|
|
48
|
+
from raceway.injector import configure_injector
|
|
49
|
+
from raceway.protocols import IContainer, ITask, IInjector, IExtractor
|
|
50
|
+
|
|
51
|
+
class ICrawlJob(ITask, Protocol):
|
|
52
|
+
def get_link(self) -> str: ...
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class CrawlJob(ICrawlJob):
|
|
56
|
+
def __hash__(self):
|
|
57
|
+
return id(self)
|
|
58
|
+
def __eq__(self, other: object):
|
|
59
|
+
return self is other
|
|
60
|
+
id: int
|
|
61
|
+
link: str
|
|
62
|
+
def get_link(self) -> str:
|
|
63
|
+
return self.link
|
|
64
|
+
|
|
65
|
+
class ICrawler(Protocol):
|
|
66
|
+
def crawl(self, delay: float) -> str: ...
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class CrawlerService(ICrawler):
|
|
70
|
+
job: ICrawlJob
|
|
71
|
+
def crawl(self, delay: float) -> str:
|
|
72
|
+
return f"Crawled {self.job.get_link()} in {delay}s."
|
|
73
|
+
|
|
74
|
+
def run_job(crawler_api: ICrawler) -> None:
|
|
75
|
+
delay = 3*random()
|
|
76
|
+
time.sleep(delay) # Blocking I/O (simulating a network request)
|
|
77
|
+
print (crawler_api.crawl(delay))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main():
|
|
81
|
+
#
|
|
82
|
+
# Setup
|
|
83
|
+
#
|
|
84
|
+
extractor_api = configure_extractor()
|
|
85
|
+
injector_api = configure_injector(extractor=extractor_api, task_proto=ICrawlJob)
|
|
86
|
+
planner = configure_planner(task_proto=ICrawlJob)
|
|
87
|
+
planner.queue_registration(
|
|
88
|
+
ICrawler,
|
|
89
|
+
Registration(
|
|
90
|
+
CrawlerService, extractor_api.extract(CrawlerService), scope="task"
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
container_api = startup(planner=planner)
|
|
94
|
+
|
|
95
|
+
# Prepare job runner for injection.
|
|
96
|
+
wrapped_with_inject = injector_api.wrap_in_inject(run_job)
|
|
97
|
+
|
|
98
|
+
#
|
|
99
|
+
# Payloads
|
|
100
|
+
#
|
|
101
|
+
links = [
|
|
102
|
+
"https://python.org",
|
|
103
|
+
"https://docs.python.org",
|
|
104
|
+
"https://peps.python.org",
|
|
105
|
+
*[f"https://example.com/{v}" for v in range(20)],
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
#
|
|
110
|
+
# Start threads for each link
|
|
111
|
+
#
|
|
112
|
+
threads = []
|
|
113
|
+
for link_id, link in enumerate(links):
|
|
114
|
+
# Using `args` to pass positional arguments and `kwargs` for keyword arguments
|
|
115
|
+
# ... in our case we are passing the args the inject function will need.
|
|
116
|
+
job = CrawlJob(link_id, link)
|
|
117
|
+
t = threading.Thread(
|
|
118
|
+
target=wrapped_with_inject,
|
|
119
|
+
args=(container_api, job),
|
|
120
|
+
)
|
|
121
|
+
threads.append(t)
|
|
122
|
+
|
|
123
|
+
# Start each thread
|
|
124
|
+
for t in threads:
|
|
125
|
+
t.start()
|
|
126
|
+
|
|
127
|
+
# Wait for all threads to finish
|
|
128
|
+
for t in threads:
|
|
129
|
+
t.join()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == '__main__':
|
|
133
|
+
main()
|
raceway-0.2.2/README.rst
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
raceway
|
|
2
|
+
#########
|
|
3
|
+
|
|
4
|
+
Minimal dependency injection, DI, system ideal for smaller systems that still need DI.
|
|
5
|
+
|
|
6
|
+
Overview
|
|
7
|
+
--------
|
|
8
|
+
|
|
9
|
+
- Defining: Define protocols and implementations.
|
|
10
|
+
- Loading: Load registrations extracted from implementations into planner.
|
|
11
|
+
- Starting: Run startup to create container from planner.
|
|
12
|
+
- Wrapping: Use injector to wrap task handlers to prepare for injection.
|
|
13
|
+
- Running: Create task for each "runner" and use it with the container to execute each injection enabled task handler.
|
|
14
|
+
|
|
15
|
+
Example
|
|
16
|
+
-------
|
|
17
|
+
|
|
18
|
+
This library is meant to be used with an external framework but this example
|
|
19
|
+
uses python's stdlib threading to demonstrate how the pieces fit together.
|
|
20
|
+
|
|
21
|
+
.. code-block:: python
|
|
22
|
+
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import Protocol
|
|
27
|
+
from random import random
|
|
28
|
+
|
|
29
|
+
from raceway.extractor import configure_extractor
|
|
30
|
+
from raceway.registration import Registration
|
|
31
|
+
from raceway.planner import configure_planner
|
|
32
|
+
from raceway.starter import startup
|
|
33
|
+
from raceway.injector import configure_injector
|
|
34
|
+
from raceway.protocols import IContainer, ITask, IInjector, IExtractor
|
|
35
|
+
|
|
36
|
+
class ICrawlJob(ITask, Protocol):
|
|
37
|
+
def get_link(self) -> str: ...
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class CrawlJob(ICrawlJob):
|
|
41
|
+
def __hash__(self):
|
|
42
|
+
return id(self)
|
|
43
|
+
def __eq__(self, other: object):
|
|
44
|
+
return self is other
|
|
45
|
+
id: int
|
|
46
|
+
link: str
|
|
47
|
+
def get_link(self) -> str:
|
|
48
|
+
return self.link
|
|
49
|
+
|
|
50
|
+
class ICrawler(Protocol):
|
|
51
|
+
def crawl(self, delay: float) -> str: ...
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class CrawlerService(ICrawler):
|
|
55
|
+
job: ICrawlJob
|
|
56
|
+
def crawl(self, delay: float) -> str:
|
|
57
|
+
return f"Crawled {self.job.get_link()} in {delay}s."
|
|
58
|
+
|
|
59
|
+
def run_job(crawler_api: ICrawler) -> None:
|
|
60
|
+
delay = 3*random()
|
|
61
|
+
time.sleep(delay) # Blocking I/O (simulating a network request)
|
|
62
|
+
print (crawler_api.crawl(delay))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main():
|
|
66
|
+
#
|
|
67
|
+
# Setup
|
|
68
|
+
#
|
|
69
|
+
extractor_api = configure_extractor()
|
|
70
|
+
injector_api = configure_injector(extractor=extractor_api, task_proto=ICrawlJob)
|
|
71
|
+
planner = configure_planner(task_proto=ICrawlJob)
|
|
72
|
+
planner.queue_registration(
|
|
73
|
+
ICrawler,
|
|
74
|
+
Registration(
|
|
75
|
+
CrawlerService, extractor_api.extract(CrawlerService), scope="task"
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
container_api = startup(planner=planner)
|
|
79
|
+
|
|
80
|
+
# Prepare job runner for injection.
|
|
81
|
+
wrapped_with_inject = injector_api.wrap_in_inject(run_job)
|
|
82
|
+
|
|
83
|
+
#
|
|
84
|
+
# Payloads
|
|
85
|
+
#
|
|
86
|
+
links = [
|
|
87
|
+
"https://python.org",
|
|
88
|
+
"https://docs.python.org",
|
|
89
|
+
"https://peps.python.org",
|
|
90
|
+
*[f"https://example.com/{v}" for v in range(20)],
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
#
|
|
95
|
+
# Start threads for each link
|
|
96
|
+
#
|
|
97
|
+
threads = []
|
|
98
|
+
for link_id, link in enumerate(links):
|
|
99
|
+
# Using `args` to pass positional arguments and `kwargs` for keyword arguments
|
|
100
|
+
# ... in our case we are passing the args the inject function will need.
|
|
101
|
+
job = CrawlJob(link_id, link)
|
|
102
|
+
t = threading.Thread(
|
|
103
|
+
target=wrapped_with_inject,
|
|
104
|
+
args=(container_api, job),
|
|
105
|
+
)
|
|
106
|
+
threads.append(t)
|
|
107
|
+
|
|
108
|
+
# Start each thread
|
|
109
|
+
for t in threads:
|
|
110
|
+
t.start()
|
|
111
|
+
|
|
112
|
+
# Wait for all threads to finish
|
|
113
|
+
for t in threads:
|
|
114
|
+
t.join()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == '__main__':
|
|
118
|
+
main()
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
[tool.black]
|
|
2
|
-
line-length = 80
|
|
3
2
|
target-version = ['py314']
|
|
4
3
|
|
|
4
|
+
|
|
5
5
|
[tool.pytest.ini_options]
|
|
6
6
|
addopts = [
|
|
7
7
|
"--import-mode=importlib",
|
|
8
8
|
]
|
|
9
|
+
|
|
9
10
|
[tool.pyright]
|
|
10
11
|
include = [
|
|
11
12
|
"src/raceway/**/*.py"
|
|
@@ -15,6 +16,7 @@ exclude = [
|
|
|
15
16
|
]
|
|
16
17
|
|
|
17
18
|
[tool.hatch]
|
|
19
|
+
installer = "pip"
|
|
18
20
|
|
|
19
21
|
[tool.hatch.env]
|
|
20
22
|
|
|
@@ -35,10 +37,38 @@ packages = ["src/raceway"]
|
|
|
35
37
|
requires = ["hatchling >= 1.26"]
|
|
36
38
|
build-backend = "hatchling.build"
|
|
37
39
|
|
|
40
|
+
[tool.hatch.envs.default]
|
|
41
|
+
dependencies = [
|
|
42
|
+
"pytest",
|
|
43
|
+
"pyright",
|
|
44
|
+
"flake8",
|
|
45
|
+
"flake8-bugbear",
|
|
46
|
+
"black",
|
|
47
|
+
"twine",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.hatch.envs.default.scripts]
|
|
51
|
+
testit="pytest tests"
|
|
52
|
+
checkit=["pyright src tests"]
|
|
53
|
+
lintit=["flake8 src tests"]
|
|
54
|
+
shapeit=["black src tests"]
|
|
55
|
+
buildit=[
|
|
56
|
+
"python -m build --wheel",
|
|
57
|
+
"python -m build --sdist",
|
|
58
|
+
"twine check dist/*.whl"
|
|
59
|
+
]
|
|
60
|
+
tagit=[
|
|
61
|
+
"git tag -a v`hatch version` -m Version' '`hatch version`",
|
|
62
|
+
"git push origin v`hatch version`"
|
|
63
|
+
]
|
|
64
|
+
releaseit=[
|
|
65
|
+
"twine upload dist/*",
|
|
66
|
+
]
|
|
38
67
|
|
|
39
68
|
[project]
|
|
40
69
|
name = "raceway"
|
|
41
70
|
description = "Simple dependency injection system. "
|
|
71
|
+
readme = "README.rst"
|
|
42
72
|
dynamic = ["version"]
|
|
43
73
|
dependencies = []
|
|
44
74
|
authors = [
|
|
@@ -51,4 +81,9 @@ classifiers = [
|
|
|
51
81
|
]
|
|
52
82
|
license = "MIT"
|
|
53
83
|
license-files = ["LICEN[CS]E*"]
|
|
54
|
-
|
|
84
|
+
|
|
85
|
+
[project.urls]
|
|
86
|
+
GitHub="https://github.com/ianjosephwilson/raceway"
|
|
87
|
+
|
|
88
|
+
[project.optional-dependencies]
|
|
89
|
+
venusian = ["venusian"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.2"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides decorators that attach callbacks that venusian can find.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from dataclasses import dataclass, is_dataclass
|
|
8
|
+
from inspect import isclass
|
|
9
|
+
|
|
10
|
+
from .exc import RacewayError
|
|
11
|
+
from .protocols import ILoader, IPlanner, IExtractor, ScopeType
|
|
12
|
+
from .registration import Registration
|
|
13
|
+
|
|
14
|
+
DEFAULT_CATEGORY = "raceway.service"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# @TODO: Make sure this the latest and greatest pattern for doing this.
|
|
18
|
+
# We don't want to raise unless someone tries to use the actual
|
|
19
|
+
# decorators without another attach.
|
|
20
|
+
try:
|
|
21
|
+
import venusian
|
|
22
|
+
|
|
23
|
+
venusian_attach = venusian.attach
|
|
24
|
+
except ImportError:
|
|
25
|
+
venusian_attach = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
LoaderCtx: ContextVar[ILoader | None] = ContextVar("LoaderCtx")
|
|
29
|
+
"""ContextVar that holds the loader during scanning for callbacks."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CallbackError(RacewayError):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Loader(ILoader):
|
|
38
|
+
planner: IPlanner
|
|
39
|
+
extractor: IExtractor
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def configure_loader(planner: IPlanner, extractor: IExtractor) -> ILoader:
|
|
43
|
+
return Loader(planner=planner, extractor=extractor)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def feed_loader(
|
|
47
|
+
loader: ILoader,
|
|
48
|
+
feed: Callable[[], None],
|
|
49
|
+
cv: ContextVar[ILoader | None] = LoaderCtx,
|
|
50
|
+
):
|
|
51
|
+
"""Setup the loader context and then call feed."""
|
|
52
|
+
with cv.set(loader):
|
|
53
|
+
feed()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def configure_as_service[T](
|
|
57
|
+
register_proto: type[T],
|
|
58
|
+
cv: ContextVar[ILoader | None] = LoaderCtx,
|
|
59
|
+
attach: Callable | None = venusian_attach,
|
|
60
|
+
scope: ScopeType = "task",
|
|
61
|
+
wrap_in_dataclass: bool = True,
|
|
62
|
+
category: str = DEFAULT_CATEGORY,
|
|
63
|
+
) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
|
64
|
+
"""
|
|
65
|
+
Decorate a service factory with a callback that can be executed to feed
|
|
66
|
+
its registration to a loader.
|
|
67
|
+
|
|
68
|
+
@NOTE: The `cv` must be set with a loader during callback's execution. This
|
|
69
|
+
can be done by passing a `feed()` function into `feed_loader` along with
|
|
70
|
+
a configured loader.
|
|
71
|
+
"""
|
|
72
|
+
if attach is None:
|
|
73
|
+
raise CallbackError("Venusian must be installed to use callbacks.")
|
|
74
|
+
|
|
75
|
+
def wrapper(
|
|
76
|
+
service_factory: Callable[..., T],
|
|
77
|
+
) -> Callable[..., T]:
|
|
78
|
+
# Wrap the service class in a dataclass here in case it is needed
|
|
79
|
+
# at anypoint in the future. This is almost just for convenience
|
|
80
|
+
# and maybe it should be moved to an extended decorator.
|
|
81
|
+
if (
|
|
82
|
+
wrap_in_dataclass
|
|
83
|
+
and isclass(service_factory)
|
|
84
|
+
and not is_dataclass(service_factory)
|
|
85
|
+
):
|
|
86
|
+
service_factory = dataclass(service_factory)
|
|
87
|
+
|
|
88
|
+
def callback(*_):
|
|
89
|
+
"""Callback for venusian scan."""
|
|
90
|
+
loader = cv.get()
|
|
91
|
+
if loader is None:
|
|
92
|
+
raise CallbackError(
|
|
93
|
+
f"{cv} context variable must be set when callback fires."
|
|
94
|
+
)
|
|
95
|
+
dep_specs = loader.extractor.extract(service_factory)
|
|
96
|
+
loader.planner.queue_registration(
|
|
97
|
+
register_proto, Registration(service_factory, dep_specs, scope=scope)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
attach(service_factory, callback, category=category)
|
|
101
|
+
return service_factory
|
|
102
|
+
|
|
103
|
+
return wrapper
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from weakref import WeakKeyDictionary, WeakValueDictionary
|
|
3
|
+
|
|
4
|
+
from .exc import RacewayError
|
|
5
|
+
from .protocols import (
|
|
6
|
+
IContainer,
|
|
7
|
+
IRegistry,
|
|
8
|
+
IRegistration,
|
|
9
|
+
IDepSpec,
|
|
10
|
+
ScopeType,
|
|
11
|
+
)
|
|
12
|
+
from .rules import can_depend_on
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ContainerError(RacewayError):
|
|
16
|
+
"""General error during container operations."""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NotSet:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
NOT_SET = NotSet()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_task_cache() -> WeakValueDictionary:
|
|
29
|
+
return WeakValueDictionary()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def create_container_cache() -> WeakKeyDictionary:
|
|
33
|
+
return WeakKeyDictionary()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def configure_container[V, W](
|
|
37
|
+
registry: IRegistry,
|
|
38
|
+
startup_task: W,
|
|
39
|
+
task_proto: type[V],
|
|
40
|
+
) -> IContainer[V, W]:
|
|
41
|
+
cache = create_container_cache()
|
|
42
|
+
return Container(
|
|
43
|
+
cache=cache, registry=registry, startup_task=startup_task, task_proto=task_proto
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Container[V, W](IContainer[V, W]):
|
|
49
|
+
|
|
50
|
+
cache: WeakKeyDictionary
|
|
51
|
+
|
|
52
|
+
registry: IRegistry
|
|
53
|
+
|
|
54
|
+
startup_task: W
|
|
55
|
+
|
|
56
|
+
task_proto: type[V]
|
|
57
|
+
|
|
58
|
+
def validate_dep_specs(
|
|
59
|
+
self,
|
|
60
|
+
dep_specs: tuple[tuple[str, IDepSpec], ...],
|
|
61
|
+
scope: ScopeType = "call",
|
|
62
|
+
) -> None:
|
|
63
|
+
for _, dep_spec in dep_specs:
|
|
64
|
+
reg = self.registry.find(dep_spec.proto)
|
|
65
|
+
if reg is None:
|
|
66
|
+
raise ContainerError(
|
|
67
|
+
f"Cannot find registration for given protocol: {dep_spec.proto}"
|
|
68
|
+
)
|
|
69
|
+
dep_scope = reg.scope
|
|
70
|
+
if not can_depend_on(scope, dep_scope):
|
|
71
|
+
raise ContainerError(
|
|
72
|
+
f"Scope mismatch: {scope} cannot depend on {dep_spec.proto}:{dep_scope}" # noqa B950
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def resolve_deps( # @TODO: Should be dep specs too
|
|
76
|
+
self,
|
|
77
|
+
dep_specs: tuple[tuple[str, IDepSpec], ...],
|
|
78
|
+
task: V | None = None,
|
|
79
|
+
) -> tuple[tuple[str, object], ...]:
|
|
80
|
+
resolved = []
|
|
81
|
+
for dep_name, dep_spec in dep_specs:
|
|
82
|
+
result = self.find_service(dep_spec.proto, task=task)
|
|
83
|
+
if dep_spec.attr:
|
|
84
|
+
result = getattr(result, dep_spec.attr)
|
|
85
|
+
if dep_spec.key:
|
|
86
|
+
result = result[dep_spec.key] # type: ignore
|
|
87
|
+
if dep_spec.call_kwargs is not None or dep_spec.call_args is not None:
|
|
88
|
+
call_kwargs = (
|
|
89
|
+
dict(dep_spec.call_kwargs)
|
|
90
|
+
if dep_spec.call_kwargs is not None
|
|
91
|
+
else {}
|
|
92
|
+
)
|
|
93
|
+
call_args = dep_spec.call_args if dep_spec.call_args is not None else ()
|
|
94
|
+
result = result(*call_args, **call_kwargs) # type: ignore
|
|
95
|
+
resolved.append((dep_name, result))
|
|
96
|
+
return tuple(resolved)
|
|
97
|
+
|
|
98
|
+
def make_service[T](
|
|
99
|
+
self,
|
|
100
|
+
reg: IRegistration[T],
|
|
101
|
+
task: V | None = None,
|
|
102
|
+
) -> T:
|
|
103
|
+
deps = dict(self.resolve_deps(reg.dep_specs, task=task))
|
|
104
|
+
return reg.factory(**deps)
|
|
105
|
+
|
|
106
|
+
def find_service[T](
|
|
107
|
+
self,
|
|
108
|
+
proto: type[T],
|
|
109
|
+
task: V | None = None,
|
|
110
|
+
) -> T:
|
|
111
|
+
if proto is self.task_proto:
|
|
112
|
+
if task is None:
|
|
113
|
+
raise ContainerError("Cannot find task because no task was provided!")
|
|
114
|
+
return task # type: ignore @TODO: Resolve this when we rewrite all the types.
|
|
115
|
+
reg = self.registry.find(proto)
|
|
116
|
+
if reg is None:
|
|
117
|
+
raise ContainerError(
|
|
118
|
+
f"Cannot find registration for given protocol: {proto}"
|
|
119
|
+
)
|
|
120
|
+
if reg.scope == "startup":
|
|
121
|
+
if self.startup_task in self.cache:
|
|
122
|
+
task_cache = self.cache[self.startup_task]
|
|
123
|
+
else:
|
|
124
|
+
maybe_cache = create_task_cache()
|
|
125
|
+
# We use the *result* of setdefault to try to avoid the race condition.
|
|
126
|
+
task_cache = self.cache.setdefault(self.startup_task, maybe_cache)
|
|
127
|
+
v = task_cache.get(proto, NOT_SET)
|
|
128
|
+
if v is NOT_SET:
|
|
129
|
+
maybe_v = self.make_service(reg, task=task)
|
|
130
|
+
v = task_cache.setdefault(proto, maybe_v)
|
|
131
|
+
return v
|
|
132
|
+
elif reg.scope == "task":
|
|
133
|
+
if task is None:
|
|
134
|
+
raise ContainerError(
|
|
135
|
+
"Cannot access task cache without providing a task!"
|
|
136
|
+
)
|
|
137
|
+
if task in self.cache:
|
|
138
|
+
task_cache = self.cache[task]
|
|
139
|
+
else:
|
|
140
|
+
# We use the *result* of setdefault to try to avoid the race condition.
|
|
141
|
+
maybe_cache = create_task_cache()
|
|
142
|
+
task_cache = self.cache.setdefault(task, maybe_cache)
|
|
143
|
+
v = task_cache.get(proto, NOT_SET)
|
|
144
|
+
if v is NOT_SET:
|
|
145
|
+
maybe_v = self.make_service(reg, task=task)
|
|
146
|
+
v = task_cache.setdefault(proto, maybe_v)
|
|
147
|
+
return v
|
|
148
|
+
else:
|
|
149
|
+
return self.make_service(reg, task=task)
|