mixinv2 0.3.0.post5.dev0__tar.gz → 0.3.0.post24.dev0__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.
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/PKG-INFO +2 -2
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/README.md +1 -1
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/docs/Makefile +1 -5
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/docs/conf.py +22 -3
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/docs/mixinv2-tutorial.rst +51 -51
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/docs/specification.md +26 -26
- mixinv2-0.3.0.post24.dev0/docs/tutorial.rst +206 -0
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/pyproject.toml +2 -2
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/src/mixinv2/_mixin_parser.py +1 -1
- mixinv2-0.3.0.post5.dev0/docs/tutorial.rst +0 -336
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/.gitignore +0 -0
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/docs/_static/favicon.svg +0 -0
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/docs/_static/logo.svg +0 -0
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/docs/index.rst +0 -0
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/docs/installation.rst +0 -0
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/src/mixinv2/__init__.py +0 -0
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/src/mixinv2/_config.py +0 -0
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/src/mixinv2/_core.py +0 -0
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/src/mixinv2/_interned_linked_list.py +0 -0
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/src/mixinv2/_mixin_directory.py +0 -0
- {mixinv2-0.3.0.post5.dev0 → mixinv2-0.3.0.post24.dev0}/src/mixinv2/_runtime.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mixinv2
|
|
3
|
-
Version: 0.3.0.
|
|
3
|
+
Version: 0.3.0.post24.dev0
|
|
4
4
|
Summary: A dependency injection framework with pytest-fixture syntax, plus a configuration language for declarative programming
|
|
5
|
-
Project-URL: Repository, https://github.com/Atry/
|
|
5
|
+
Project-URL: Repository, https://github.com/Atry/MIXINv2
|
|
6
6
|
Author-email: "Yang, Bo" <yang-bo@yang-bo.com>
|
|
7
7
|
License-Expression: MIT
|
|
8
8
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -10,7 +10,7 @@ Full documentation is available at [mixinv2.readthedocs.io](https://mixinv2.read
|
|
|
10
10
|
|
|
11
11
|
## Source Code
|
|
12
12
|
|
|
13
|
-
The source code is hosted on [GitHub](https://github.com/Atry/
|
|
13
|
+
The source code is hosted on [GitHub](https://github.com/Atry/MIXINv2).
|
|
14
14
|
|
|
15
15
|
## License
|
|
16
16
|
|
|
@@ -16,9 +16,5 @@ help:
|
|
|
16
16
|
|
|
17
17
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
|
18
18
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
|
19
|
-
%: Makefile
|
|
19
|
+
%: Makefile
|
|
20
20
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
21
|
-
|
|
22
|
-
.PHONY: api-docs
|
|
23
|
-
api-docs:
|
|
24
|
-
sphinx-apidoc --implicit-namespaces -o api ../src/overlay
|
|
@@ -7,6 +7,8 @@ import subprocess
|
|
|
7
7
|
import sys
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
from sphinx.ext import apidoc
|
|
11
|
+
|
|
10
12
|
# -- Path setup --------------------------------------------------------------
|
|
11
13
|
# Add mixinv2/src to sys.path so autodoc can import the modules.
|
|
12
14
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
|
|
@@ -37,7 +39,7 @@ autodoc_default_options = {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
templates_path = ['_templates']
|
|
40
|
-
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'api/modules.rst'
|
|
42
|
+
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'api/modules.rst']
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
|
|
@@ -53,7 +55,7 @@ html_theme_options = {
|
|
|
53
55
|
'logo': 'logo.svg',
|
|
54
56
|
'fixed_sidebar': True,
|
|
55
57
|
'github_user': 'Atry',
|
|
56
|
-
'github_repo': '
|
|
58
|
+
'github_repo': 'MIXINv2',
|
|
57
59
|
'github_banner': True,
|
|
58
60
|
'github_button': True,
|
|
59
61
|
'github_type': 'watch',
|
|
@@ -68,7 +70,24 @@ _git_commit = subprocess.check_output(
|
|
|
68
70
|
|
|
69
71
|
extlinks = {
|
|
70
72
|
'github': (
|
|
71
|
-
f'https://github.com/Atry/
|
|
73
|
+
f'https://github.com/Atry/MIXINv2/tree/{_git_commit}/%s',
|
|
72
74
|
'%s',
|
|
73
75
|
),
|
|
74
76
|
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _generate_api_docs(app: object) -> None:
|
|
80
|
+
docs_directory = Path(__file__).resolve().parent
|
|
81
|
+
package_directory = docs_directory.parent / "src" / "mixinv2"
|
|
82
|
+
output_directory = docs_directory / "api"
|
|
83
|
+
|
|
84
|
+
apidoc.main([
|
|
85
|
+
"--implicit-namespaces",
|
|
86
|
+
"-o",
|
|
87
|
+
str(output_directory),
|
|
88
|
+
str(package_directory),
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def setup(app: object) -> None:
|
|
93
|
+
app.connect("builder-inited", _generate_api_docs)
|
|
@@ -21,7 +21,7 @@ MIXINv2 solves this by separating the application into three layers:
|
|
|
21
21
|
per operation (``sqlite3.connect``, ``str.split``, ``wfile.write``). Each adapter
|
|
22
22
|
declares its inputs as ``@extern`` and exposes a single ``@public @resource``
|
|
23
23
|
output. The adapter contains **zero business logic**.
|
|
24
|
-
-
|
|
24
|
+
- ``.mixin.yaml`` files contain all application logic, written in MIXINv2. MIXINv2 is not just a configuration format — it is a
|
|
25
25
|
complete language with lexical scoping, nested scopes, deep-merge composition,
|
|
26
26
|
and lazy evaluation. These features make it more natural than Python for
|
|
27
27
|
expressing business logic, which is inherently declarative ("the user ID is
|
|
@@ -30,8 +30,8 @@ MIXINv2 solves this by separating the application into three layers:
|
|
|
30
30
|
- **Configuration values** (SQL queries, format strings, host/port) are pure
|
|
31
31
|
YAML scalars, gathered in one place.
|
|
32
32
|
|
|
33
|
-
Business logic written in ``.
|
|
34
|
-
Python FFI layer. Swap the FFI adapters and the same ``.
|
|
33
|
+
Business logic written in ``.mixin.yaml`` is **portable**: it is decoupled from the
|
|
34
|
+
Python FFI layer. Swap the FFI adapters and the same ``.mixin.yaml`` logic runs against
|
|
35
35
|
a different runtime — mock adapters for unit testing, a different language's
|
|
36
36
|
stdlib for cross-platform deployment, or instrumented adapters for profiling.
|
|
37
37
|
With Python ``@scope`` decorators, business logic is locked to the Python runtime
|
|
@@ -45,64 +45,64 @@ Python FFI adapters
|
|
|
45
45
|
|
|
46
46
|
Each ``@scope`` class wraps exactly one stdlib operation. Below are three
|
|
47
47
|
representative adapters; the full module
|
|
48
|
-
(:github:`
|
|
48
|
+
(:github:`packages/mixinv2-examples/src/mixinv2_examples/app_mixin/StdlibFFI/FFI/`)
|
|
49
49
|
contains 10 more following the same pattern.
|
|
50
50
|
|
|
51
51
|
``SqliteConnectAndExecuteScript`` — multiple inputs, single output:
|
|
52
52
|
|
|
53
|
-
.. literalinclude:: ../../
|
|
53
|
+
.. literalinclude:: ../../mixinv2-examples/src/mixinv2_examples/app_mixin/StdlibFFI/FFI/SqliteConnectAndExecuteScript.py
|
|
54
54
|
:language: python
|
|
55
55
|
|
|
56
56
|
``GetItem`` — generic ``sequence[index]`` operation:
|
|
57
57
|
|
|
58
|
-
.. literalinclude:: ../../
|
|
58
|
+
.. literalinclude:: ../../mixinv2-examples/src/mixinv2_examples/app_mixin/StdlibFFI/FFI/GetItem.py
|
|
59
59
|
:language: python
|
|
60
60
|
|
|
61
61
|
``HttpSendResponse`` — chained I/O, send status + headers + body:
|
|
62
62
|
|
|
63
|
-
.. literalinclude:: ../../
|
|
63
|
+
.. literalinclude:: ../../mixinv2-examples/src/mixinv2_examples/app_mixin/StdlibFFI/FFI/HttpSendResponse.py
|
|
64
64
|
:language: python
|
|
65
65
|
|
|
66
66
|
Notice what is *not* here: no SQL queries, no ``"/"`` separator, no format string,
|
|
67
67
|
no ``:memory:`` path, no port number. Those are all business decisions that live
|
|
68
|
-
in the ``.
|
|
68
|
+
in the ``.mixin.yaml``.
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
``.
|
|
72
|
-
|
|
71
|
+
``.mixin.yaml`` composition
|
|
72
|
+
---------------------------
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
A ``.mixin.yaml`` file describes a **dependency graph**, not an execution sequence.
|
|
75
75
|
There is no top-to-bottom control flow — the runtime evaluates resources lazily,
|
|
76
76
|
on demand. Think spreadsheet cells, not shell scripts.
|
|
77
77
|
|
|
78
|
-
The business logic lives in ``Library.
|
|
78
|
+
The business logic lives in ``Library.mixin.yaml``, which references FFI adapters
|
|
79
79
|
through abstract declarations (``FFI:`` scope with ``[]`` slots). A concrete FFI
|
|
80
|
-
module (``
|
|
80
|
+
module (``StdlibFFI/FFI/``) overrides these slots at composition time. This
|
|
81
81
|
separation means the business logic is portable — swap ``stdlib_ffi`` for a
|
|
82
|
-
different FFI implementation and the ``.
|
|
82
|
+
different FFI implementation and the ``.mixin.yaml`` files need no changes.
|
|
83
83
|
|
|
84
|
-
The following sections walk through ``Library.
|
|
84
|
+
The following sections walk through ``Library.mixin.yaml`` one scope at a time,
|
|
85
85
|
introducing new language concepts as they appear.
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
``SQLiteDatabase`` — extern, inheritance, wiring, projection
|
|
89
89
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
90
90
|
|
|
91
|
-
.. literalinclude:: ../../
|
|
91
|
+
.. literalinclude:: ../../mixinv2-examples/src/mixinv2_examples/app_mixin/Library.mixin.yaml
|
|
92
92
|
:language: yaml
|
|
93
93
|
:start-after: # [docs:sqlite-database]
|
|
94
94
|
:end-before: # [/docs:sqlite-database]
|
|
95
95
|
|
|
96
96
|
Four new concepts:
|
|
97
97
|
|
|
98
|
-
-
|
|
98
|
+
- ``field: []`` — an **extern declaration**, the ``.mixin.yaml`` equivalent of
|
|
99
99
|
``@extern``. The value must come from a parent scope or the caller.
|
|
100
|
-
-
|
|
100
|
+
- ``- [FFI, SqliteConnectAndExecuteScript]`` — **inheritance**. ``_db`` inherits
|
|
101
101
|
the FFI adapter, gaining all of its resources (``connection``).
|
|
102
|
-
-
|
|
102
|
+
- ``database_path: [database_path]`` — **wiring**. The reference ``[database_path]``
|
|
103
103
|
is a lexical lookup: search outward through enclosing scopes until a field
|
|
104
104
|
named ``database_path`` is found.
|
|
105
|
-
-
|
|
105
|
+
- ``connection: [_db, connection]`` — **path navigation**. Access the
|
|
106
106
|
``connection`` resource on the child scope ``_db``. The leading underscore on
|
|
107
107
|
``_db`` makes it private; ``connection`` is the public-facing projection.
|
|
108
108
|
|
|
@@ -110,16 +110,16 @@ Four new concepts:
|
|
|
110
110
|
``UserRepository`` (app-scoped part) — nested scope, scope-as-dataclass
|
|
111
111
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
112
112
|
|
|
113
|
-
.. literalinclude:: ../../
|
|
113
|
+
.. literalinclude:: ../../mixinv2-examples/src/mixinv2_examples/app_mixin/Library.mixin.yaml
|
|
114
114
|
:language: yaml
|
|
115
115
|
:start-after: # [docs:user-repository-app]
|
|
116
116
|
:end-before: # [/docs:user-repository-app]
|
|
117
117
|
|
|
118
|
-
-
|
|
118
|
+
- ``User:`` is a **nested scope** with two extern fields — the ``.mixin.yaml``
|
|
119
119
|
equivalent of ``@scope class User`` with ``@public @extern`` fields. It acts as a
|
|
120
120
|
dataclass constructor: ``current_user`` (below) will supply values for
|
|
121
121
|
``user_id`` and ``name``.
|
|
122
|
-
-
|
|
122
|
+
- ``connection: []`` declares that ``UserRepository`` expects a ``connection``
|
|
123
123
|
from outside. When composed with ``SQLiteDatabase`` inside ``app``, this extern
|
|
124
124
|
is satisfied by ``SQLiteDatabase.connection`` — resolved by name through
|
|
125
125
|
lexical scoping.
|
|
@@ -128,7 +128,7 @@ Four new concepts:
|
|
|
128
128
|
``UserRepository.RequestScope`` — ANF style, cross-scope references
|
|
129
129
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
130
130
|
|
|
131
|
-
.. literalinclude:: ../../
|
|
131
|
+
.. literalinclude:: ../../mixinv2-examples/src/mixinv2_examples/app_mixin/Library.mixin.yaml
|
|
132
132
|
:language: yaml
|
|
133
133
|
:start-after: # [docs:user-repository-request-scope]
|
|
134
134
|
:end-before: # [/docs:user-repository-request-scope]
|
|
@@ -146,7 +146,7 @@ searches outward through parent, grandparent, etc. No import statement is
|
|
|
146
146
|
needed; the scope hierarchy *is* the namespace.
|
|
147
147
|
|
|
148
148
|
**Constructing ``current_user``:** Instead of calling ``User(user_id=..., name=...)``,
|
|
149
|
-
the ``.
|
|
149
|
+
the ``.mixin.yaml`` directly defines ``current_user`` as a scope with two fields. The
|
|
150
150
|
``User`` scope-as-dataclass above establishes the field names; here those same
|
|
151
151
|
names are filled with concrete values.
|
|
152
152
|
|
|
@@ -154,7 +154,7 @@ names are filled with concrete values.
|
|
|
154
154
|
``HttpHandlers`` — flat inheritance, qualified this
|
|
155
155
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
156
156
|
|
|
157
|
-
.. literalinclude:: ../../
|
|
157
|
+
.. literalinclude:: ../../mixinv2-examples/src/mixinv2_examples/app_mixin/Library.mixin.yaml
|
|
158
158
|
:language: yaml
|
|
159
159
|
:start-after: # [docs:http-handlers]
|
|
160
160
|
:end-before: # [/docs:http-handlers]
|
|
@@ -170,7 +170,7 @@ namespace. The last list item (the mapping starting with ``request: []``) define
|
|
|
170
170
|
point ``user_count`` is just an extern ``[]`` — its actual value comes from
|
|
171
171
|
``UserRepository`` after deep merge (explained below).
|
|
172
172
|
|
|
173
|
-
**Qualified this
|
|
173
|
+
**Qualified this:** ``[RequestScope, ~, written]`` — instead of declaring
|
|
174
174
|
``written: []`` and writing ``response: [written]``, this navigates the runtime
|
|
175
175
|
composition graph to access the ``written`` property inherited from
|
|
176
176
|
``HttpSendResponse``. The advantage: if ``HttpSendResponse`` is accidentally not
|
|
@@ -180,26 +180,26 @@ composed, this fails with an error instead of silently creating an empty scope.
|
|
|
180
180
|
``NetworkServer`` — deep merge, config scoping
|
|
181
181
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
182
182
|
|
|
183
|
-
.. literalinclude:: ../../
|
|
183
|
+
.. literalinclude:: ../../mixinv2-examples/src/mixinv2_examples/app_mixin/Library.mixin.yaml
|
|
184
184
|
:language: yaml
|
|
185
185
|
:start-after: # [docs:network-server]
|
|
186
186
|
:end-before: # [/docs:network-server]
|
|
187
187
|
|
|
188
|
-
All the scopes above live in ``Library.
|
|
188
|
+
All the scopes above live in ``Library.mixin.yaml``. They reference ``[FFI, Xxx]`` which
|
|
189
189
|
resolves to abstract declarations at the top of the file — no concrete Python
|
|
190
190
|
code is involved yet.
|
|
191
191
|
|
|
192
192
|
|
|
193
|
-
``Apps.
|
|
194
|
-
|
|
193
|
+
``Apps.mixin.yaml`` — integration entry point
|
|
194
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
195
195
|
|
|
196
|
-
``Apps.
|
|
196
|
+
``Apps.mixin.yaml`` is a separate file that inherits the real FFI implementation and
|
|
197
197
|
the Library, then defines concrete application entries:
|
|
198
198
|
|
|
199
|
-
.. literalinclude:: ../../
|
|
199
|
+
.. literalinclude:: ../../mixinv2-examples/src/mixinv2_examples/app_mixin/Apps.mixin.yaml
|
|
200
200
|
:language: yaml
|
|
201
|
-
:start-after: # [docs:apps-
|
|
202
|
-
:end-before: # [/docs:apps-
|
|
201
|
+
:start-after: # [docs:apps-mixin-yaml]
|
|
202
|
+
:end-before: # [/docs:apps-mixin-yaml]
|
|
203
203
|
|
|
204
204
|
**Library/FFI separation:** ``- [stdlib_ffi]`` makes the real ``FFI`` module
|
|
205
205
|
(Python ``@scope`` classes) visible. ``- [Library]`` makes the business logic
|
|
@@ -208,21 +208,21 @@ with ``Library.FFI`` (abstract declarations), and the real ``@resource`` methods
|
|
|
208
208
|
override the ``[]`` slots. The business logic never imports Python directly.
|
|
209
209
|
|
|
210
210
|
**Portability:** To run the same business logic on a different runtime, replace
|
|
211
|
-
``- [stdlib_ffi]`` with a different FFI package — the ``Library.
|
|
211
|
+
``- [stdlib_ffi]`` with a different FFI package — the ``Library.mixin.yaml`` file needs
|
|
212
212
|
no changes. To test with mocks, provide a mock FFI module instead of
|
|
213
213
|
``stdlib_ffi``.
|
|
214
214
|
|
|
215
215
|
**"What Color Is Your Function?"** (`blog post <https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/>`_):
|
|
216
|
-
The same ``Library.
|
|
216
|
+
The same ``Library.mixin.yaml`` runs unchanged on both synchronous and asynchronous
|
|
217
217
|
runtimes. Replacing ``- [stdlib_ffi]`` with ``- [async_ffi]`` swaps the FFI layer
|
|
218
218
|
to one built on ``aiosqlite`` + ``starlette``
|
|
219
|
-
(:github:`implementation <
|
|
219
|
+
(:github:`implementation <packages/mixinv2-examples/src/mixinv2_examples/app_mixin/AsyncFFI/FFI/>`) — the business
|
|
220
220
|
logic never knows whether it is sync or async. Function color is confined
|
|
221
|
-
entirely to the FFI boundary; ``Library.
|
|
221
|
+
entirely to the FFI boundary; ``Library.mixin.yaml`` itself is *colorless*.
|
|
222
222
|
|
|
223
223
|
**Composition via inheritance:** ``memory_app`` inherits four scopes via
|
|
224
224
|
qualified this (``[Apps, ~, SQLiteDatabase]`` etc.) because these scopes are
|
|
225
|
-
inherited properties, not own properties of ``Apps.
|
|
225
|
+
inherited properties, not own properties of ``Apps.mixin.yaml``. This is not four
|
|
226
226
|
separate instances — it is a single scope with all four merged together. The
|
|
227
227
|
last list item supplies concrete values for every ``[]`` extern.
|
|
228
228
|
|
|
@@ -281,7 +281,7 @@ Python vs MIXINv2
|
|
|
281
281
|
|
|
282
282
|
* - Aspect
|
|
283
283
|
- Python ``@scope``
|
|
284
|
-
- MIXINv2 (``.
|
|
284
|
+
- MIXINv2 (``.mixin.yaml``)
|
|
285
285
|
* - Composition
|
|
286
286
|
- Manual ``@extend`` + ``RelativeReference``
|
|
287
287
|
- Inheritance list: ``- [Parent]``
|
|
@@ -299,7 +299,7 @@ Python vs MIXINv2
|
|
|
299
299
|
- Qualified this: ``[Scope, ~, symbol]``
|
|
300
300
|
* - Business logic location
|
|
301
301
|
- Mixed with I/O in Python
|
|
302
|
-
- Separate ``.
|
|
302
|
+
- Separate ``.mixin.yaml`` file, portable across FFI
|
|
303
303
|
* - Configuration
|
|
304
304
|
- Kwargs at call site
|
|
305
305
|
- Scalar values in ``memory_app:`` scope
|
|
@@ -310,13 +310,13 @@ Evaluation
|
|
|
310
310
|
|
|
311
311
|
.. code-block:: python
|
|
312
312
|
|
|
313
|
-
import tests.fixtures.
|
|
313
|
+
import tests.fixtures.app_mixin as app_mixin
|
|
314
314
|
from mixinv2 import evaluate
|
|
315
315
|
|
|
316
|
-
# evaluate() auto-discovers stdlib_ffi/, Library.
|
|
317
|
-
root = evaluate(
|
|
316
|
+
# evaluate() auto-discovers stdlib_ffi/, Library.mixin.yaml, and Apps.mixin.yaml.
|
|
317
|
+
root = evaluate(app_mixin, modules_public=True)
|
|
318
318
|
|
|
319
|
-
# Access the composed app — Apps is the .
|
|
319
|
+
# Access the composed app — Apps is the .mixin.yaml file name.
|
|
320
320
|
composed_app = root.Apps.memory_app
|
|
321
321
|
|
|
322
322
|
composed_app.server # HTTPServer on 127.0.0.1:<assigned port>
|
|
@@ -328,14 +328,14 @@ Evaluation
|
|
|
328
328
|
scope.current_user.name # "alice"
|
|
329
329
|
scope.response # sends HTTP response as side effect
|
|
330
330
|
|
|
331
|
-
Swapping configuration is just a different entry in ``Apps.
|
|
332
|
-
FFI adapters and ``Library.
|
|
333
|
-
different ``@scope`` module — the ``.
|
|
331
|
+
Swapping configuration is just a different entry in ``Apps.mixin.yaml`` — the Python
|
|
332
|
+
FFI adapters and ``Library.mixin.yaml`` never change. Swapping the FFI layer is just a
|
|
333
|
+
different ``@scope`` module — the ``.mixin.yaml`` business logic never changes.
|
|
334
334
|
|
|
335
335
|
Runnable tests for this example are in
|
|
336
|
-
:github:`tests/test_readme_package_examples.py`,
|
|
336
|
+
:github:`packages/mixinv2-examples/tests/test_readme_package_examples.py`,
|
|
337
337
|
using the fixture package at
|
|
338
|
-
:github:`
|
|
338
|
+
:github:`packages/mixinv2-examples/src/mixinv2_examples/app_mixin/`.
|
|
339
339
|
|
|
340
340
|
The full language specification is in :doc:`specification`.
|
|
341
341
|
|
|
@@ -91,7 +91,7 @@ As a configuration language, MIXINv2 excels in defining complex systems with int
|
|
|
91
91
|
The following example demonstrates how to use MIXINv2 to define a basic arithmetic operation represented as an AST:
|
|
92
92
|
|
|
93
93
|
```yaml
|
|
94
|
-
# math_operations.
|
|
94
|
+
# math_operations.mixin.yaml
|
|
95
95
|
|
|
96
96
|
Number:
|
|
97
97
|
- {} # An overlay that represents a number type.
|
|
@@ -108,7 +108,7 @@ multiply:
|
|
|
108
108
|
```
|
|
109
109
|
|
|
110
110
|
```yaml
|
|
111
|
-
# test.
|
|
111
|
+
# test.mixin.yaml
|
|
112
112
|
|
|
113
113
|
example_calculation:
|
|
114
114
|
- [add]
|
|
@@ -124,7 +124,7 @@ example_calculation:
|
|
|
124
124
|
1. The `Number` overlay represents a basic number type with no initial value, aligning with MIXINv2's immutable and lazy-evaluated nature.
|
|
125
125
|
2. The `add` overlay inherits from `Number` and defines two properties, `addend1` and `addend2`, both of which are also `Number`.
|
|
126
126
|
3. The `multiply` overlay defines a multiplication operation with two properties: `multiplicand` and `multiplier`.
|
|
127
|
-
4. In `test.
|
|
127
|
+
4. In `test.mixin.yaml`, the `example_calculation` overlay uses the `add` operation to add two numbers:
|
|
128
128
|
- `addend1` is a multiplication of `2` and `3`, represented using the `multiply` overlay.
|
|
129
129
|
- `addend2` is the constant `4`.
|
|
130
130
|
|
|
@@ -286,7 +286,7 @@ In this example, `my_number` has both a scalar value `42` and inherits the `Numb
|
|
|
286
286
|
In MIXINv2, properties with the same name defined in multiple parent overlays are always automatically merged:
|
|
287
287
|
|
|
288
288
|
```yaml
|
|
289
|
-
# basic_features.
|
|
289
|
+
# basic_features.mixin.yaml
|
|
290
290
|
Vehicle:
|
|
291
291
|
- wheels: [Number]
|
|
292
292
|
- engine: {}
|
|
@@ -295,7 +295,7 @@ Motor:
|
|
|
295
295
|
- engine:
|
|
296
296
|
gasoline: true # Defines a default scalar value for 'engine'
|
|
297
297
|
|
|
298
|
-
# advanced_features.
|
|
298
|
+
# advanced_features.mixin.yaml
|
|
299
299
|
hybrid_car:
|
|
300
300
|
- ["basic_features", Vehicle]
|
|
301
301
|
- ["basic_features", Motor]
|
|
@@ -464,17 +464,17 @@ MIXINv2 allows inheriting overlays defined in different files. The rules for cro
|
|
|
464
464
|
project/
|
|
465
465
|
│
|
|
466
466
|
├── module/
|
|
467
|
-
│ ├── vehicle.
|
|
468
|
-
│ ├── electric.
|
|
469
|
-
│ └── car.
|
|
467
|
+
│ ├── vehicle.mixin.yaml
|
|
468
|
+
│ ├── electric.mixin.yaml
|
|
469
|
+
│ └── car.mixin.yaml
|
|
470
470
|
│
|
|
471
471
|
├── config/
|
|
472
|
-
│ └── settings.
|
|
472
|
+
│ └── settings.mixin.yaml
|
|
473
473
|
└── test/
|
|
474
|
-
└── test_car.
|
|
474
|
+
└── test_car.mixin.yaml
|
|
475
475
|
```
|
|
476
476
|
|
|
477
|
-
**vehicle.
|
|
477
|
+
**vehicle.mixin.yaml**:
|
|
478
478
|
|
|
479
479
|
```yaml
|
|
480
480
|
Vehicle:
|
|
@@ -482,7 +482,7 @@ Vehicle:
|
|
|
482
482
|
wheels: [Number]
|
|
483
483
|
```
|
|
484
484
|
|
|
485
|
-
**electric.
|
|
485
|
+
**electric.mixin.yaml**:
|
|
486
486
|
|
|
487
487
|
```yaml
|
|
488
488
|
Electric:
|
|
@@ -491,7 +491,7 @@ Electric:
|
|
|
491
491
|
- battery_capacity: [Number]
|
|
492
492
|
```
|
|
493
493
|
|
|
494
|
-
**car.
|
|
494
|
+
**car.mixin.yaml**:
|
|
495
495
|
|
|
496
496
|
```yaml
|
|
497
497
|
Car:
|
|
@@ -500,11 +500,11 @@ Car:
|
|
|
500
500
|
- model: [String]
|
|
501
501
|
```
|
|
502
502
|
|
|
503
|
-
**test_car.
|
|
503
|
+
**test_car.mixin.yaml**:
|
|
504
504
|
|
|
505
505
|
```yaml
|
|
506
506
|
test_car:
|
|
507
|
-
- [module, Car] # Cross-directory inheritance to Car in module/car.
|
|
507
|
+
- [module, Car] # Cross-directory inheritance to Car in module/car.mixin.yaml
|
|
508
508
|
- model: "Test Model"
|
|
509
509
|
- test_battery:
|
|
510
510
|
- [module, Electric, battery_capacity] # Inheritance to battery_capacity in Electric
|
|
@@ -512,7 +512,7 @@ test_car:
|
|
|
512
512
|
|
|
513
513
|
In this example:
|
|
514
514
|
|
|
515
|
-
- The `test_car` overlay in `test_car.
|
|
515
|
+
- The `test_car` overlay in `test_car.mixin.yaml` inherits `Car` and `Electric` from the `module` directory using the format `[module, Car]` and `[module, Electric, battery_capacity]`.
|
|
516
516
|
- The first segment of the inheritance (`module`) indicates the directory in which the target overlays are located.
|
|
517
517
|
- The inheritance format and scope rules ensure that overlays are correctly resolved based on the file and directory structure.
|
|
518
518
|
|
|
@@ -704,7 +704,7 @@ When an overlay inherits from multiple parent overlays, the properties from all
|
|
|
704
704
|
**Example:**
|
|
705
705
|
|
|
706
706
|
```yaml
|
|
707
|
-
# basic_features.
|
|
707
|
+
# basic_features.mixin.yaml
|
|
708
708
|
Vehicle:
|
|
709
709
|
- wheels: [Number]
|
|
710
710
|
- engine: {}
|
|
@@ -713,7 +713,7 @@ Motor:
|
|
|
713
713
|
- engine:
|
|
714
714
|
gasoline: true # Scalar value for 'engine'
|
|
715
715
|
|
|
716
|
-
# advanced_features.
|
|
716
|
+
# advanced_features.mixin.yaml
|
|
717
717
|
hybrid_car:
|
|
718
718
|
- ["basic_features", Vehicle]
|
|
719
719
|
- ["basic_features", Motor]
|
|
@@ -740,15 +740,15 @@ Scalar values (e.g., strings, numbers, booleans) can coexist with properties wit
|
|
|
740
740
|
**Example:**
|
|
741
741
|
|
|
742
742
|
```yaml
|
|
743
|
-
# number.
|
|
743
|
+
# number.mixin.yaml
|
|
744
744
|
Number:
|
|
745
745
|
- {} # Represents a number type overlay
|
|
746
746
|
|
|
747
|
-
# value.
|
|
747
|
+
# value.mixin.yaml
|
|
748
748
|
value_42:
|
|
749
749
|
- 42 # Defines scalar value 42
|
|
750
750
|
|
|
751
|
-
# my_number.
|
|
751
|
+
# my_number.mixin.yaml
|
|
752
752
|
my_number:
|
|
753
753
|
- [Number]
|
|
754
754
|
- [value_42]
|
|
@@ -770,15 +770,15 @@ An overlay can have both scalar values and properties, and these can be inherite
|
|
|
770
770
|
**Example:**
|
|
771
771
|
|
|
772
772
|
```yaml
|
|
773
|
-
# person.
|
|
773
|
+
# person.mixin.yaml
|
|
774
774
|
PersonDetails:
|
|
775
775
|
name: [String]
|
|
776
776
|
age: [Number]
|
|
777
777
|
|
|
778
|
-
# height.
|
|
778
|
+
# height.mixin.yaml
|
|
779
779
|
height_value: 180 # Scalar value representing height
|
|
780
780
|
|
|
781
|
-
# combined_person.
|
|
781
|
+
# combined_person.mixin.yaml
|
|
782
782
|
combined_person:
|
|
783
783
|
- [PersonDetails]
|
|
784
784
|
- name: "John Doe"
|
|
@@ -806,7 +806,7 @@ MIXINv2's approach to inheritance ensures that properties and scalar values from
|
|
|
806
806
|
**Example of Conflict-Free Inheritance:**
|
|
807
807
|
|
|
808
808
|
```yaml
|
|
809
|
-
# basic_features.
|
|
809
|
+
# basic_features.mixin.yaml
|
|
810
810
|
Vehicle:
|
|
811
811
|
- wheels: [Number]
|
|
812
812
|
- engine:
|
|
@@ -815,7 +815,7 @@ Vehicle:
|
|
|
815
815
|
Motor:
|
|
816
816
|
- engine: {} # Defines 'engine' property
|
|
817
817
|
|
|
818
|
-
# advanced_features.
|
|
818
|
+
# advanced_features.mixin.yaml
|
|
819
819
|
hybrid_car:
|
|
820
820
|
- ["basic_features", Vehicle]
|
|
821
821
|
- ["basic_features", Motor]
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
Getting Started with Decorators
|
|
2
|
+
================================
|
|
3
|
+
|
|
4
|
+
The examples below build a single web application step by step, introducing one
|
|
5
|
+
concept at a time. All code is runnable with the standard library only.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Step 1 — Define services
|
|
9
|
+
------------------------
|
|
10
|
+
|
|
11
|
+
Decorate a class with ``@scope`` to make it a DI container. Annotate each value with
|
|
12
|
+
``@resource`` and expose it with ``@public``. Resources declare their dependencies as
|
|
13
|
+
ordinary function parameters; the framework injects them by name.
|
|
14
|
+
|
|
15
|
+
Use ``@extern`` to declare a dependency that must come from outside the scope — the
|
|
16
|
+
equivalent of a pytest fixture parameter. Pass multiple scopes to ``evaluate()`` to
|
|
17
|
+
compose them; dependencies are resolved by name across scope boundaries. Config
|
|
18
|
+
values are passed as kwargs when calling the evaluated scope.
|
|
19
|
+
|
|
20
|
+
.. literalinclude:: ../../mixinv2-examples/src/mixinv2_examples/app_decorator/step1_services.py
|
|
21
|
+
:language: python
|
|
22
|
+
:start-after: # [docs:step1-define-services]
|
|
23
|
+
:end-before: # [/docs:step1-define-services]
|
|
24
|
+
:dedent:
|
|
25
|
+
|
|
26
|
+
``SQLiteDatabase`` owns ``databasePath``; ``UserRepository`` has no knowledge of the
|
|
27
|
+
database layer — it only declares ``connection: sqlite3.Connection`` as a parameter
|
|
28
|
+
and receives it automatically from the composed scope.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
Step 2 — Layer cross-cutting concerns with ``@patch`` and ``@merge``
|
|
32
|
+
--------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
``@patch`` wraps an existing resource value with a transformation. This lets an
|
|
35
|
+
add-on scope modify a value without touching the scope that defined it — the same
|
|
36
|
+
idea as pytest's ``monkeypatch``, but composable.
|
|
37
|
+
|
|
38
|
+
.. literalinclude:: ../../mixinv2-examples/src/mixinv2_examples/app_decorator/step2_patch.py
|
|
39
|
+
:language: python
|
|
40
|
+
:start-after: # [docs:step2-patch]
|
|
41
|
+
:end-before: # [/docs:step2-patch]
|
|
42
|
+
:dedent:
|
|
43
|
+
|
|
44
|
+
When several independent scopes each contribute a piece to the same resource, use
|
|
45
|
+
``@merge`` to define how the contributions are aggregated:
|
|
46
|
+
|
|
47
|
+
.. literalinclude:: ../../mixinv2-examples/src/mixinv2_examples/app_decorator/step2_merge.py
|
|
48
|
+
:language: python
|
|
49
|
+
:start-after: # [docs:step2-merge]
|
|
50
|
+
:end-before: # [/docs:step2-merge]
|
|
51
|
+
:dedent:
|
|
52
|
+
|
|
53
|
+
A ``@patch`` can itself declare ``@extern`` dependencies, which are injected like any
|
|
54
|
+
other resource:
|
|
55
|
+
|
|
56
|
+
.. literalinclude:: ../../mixinv2-examples/src/mixinv2_examples/app_decorator/step2_patch_extern.py
|
|
57
|
+
:language: python
|
|
58
|
+
:start-after: # [docs:step2-patch-extern]
|
|
59
|
+
:end-before: # [/docs:step2-patch-extern]
|
|
60
|
+
:dedent:
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
Step 3 — Force evaluation at startup with ``@eager``
|
|
64
|
+
-----------------------------------------------------
|
|
65
|
+
|
|
66
|
+
All resources are lazy by default: computed on first access, then cached for the
|
|
67
|
+
lifetime of the scope. Mark a resource ``@eager`` to evaluate it immediately when
|
|
68
|
+
``evaluate()`` returns — useful for schema migrations or connection pre-warming that
|
|
69
|
+
must complete before the application starts serving requests:
|
|
70
|
+
|
|
71
|
+
.. literalinclude:: ../../mixinv2-examples/src/mixinv2_examples/app_decorator/step3_eager.py
|
|
72
|
+
:language: python
|
|
73
|
+
:start-after: # [docs:step3-eager]
|
|
74
|
+
:end-before: # [/docs:step3-eager]
|
|
75
|
+
:dedent:
|
|
76
|
+
|
|
77
|
+
Without ``@eager``, the ``CREATE TABLE`` would not run until ``root.connection`` is first
|
|
78
|
+
accessed.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
Step 4 — App scope vs request scope
|
|
82
|
+
------------------------------------
|
|
83
|
+
|
|
84
|
+
So far all resources have had application lifetime: created once at startup and
|
|
85
|
+
reused for every request. Real applications also need per-request resources — values
|
|
86
|
+
that must be created fresh for each incoming request and discarded when it completes.
|
|
87
|
+
|
|
88
|
+
A nested ``@scope`` named ``RequestScope`` serves as a per-request factory. The
|
|
89
|
+
framework injects it by name as a ``Callable``; calling
|
|
90
|
+
``RequestScope(request=handler)`` returns a fresh instance.
|
|
91
|
+
|
|
92
|
+
The application below has four scopes, each owning only its own concern:
|
|
93
|
+
|
|
94
|
+
- **SQLiteDatabase** — owns ``databasePath``, provides ``connection``
|
|
95
|
+
- **UserRepository** — business logic; owns ``userCount`` and per-request ``currentUser``
|
|
96
|
+
- **HttpHandlers** — HTTP layer; owns per-request ``userId``, ``responseBody``, ``responseSent``
|
|
97
|
+
- **NetworkServer** — network layer; owns ``host``/``port``, creates the ``HTTPServer``
|
|
98
|
+
|
|
99
|
+
``UserRepository.RequestScope`` and ``HttpHandlers.RequestScope`` are composed into a
|
|
100
|
+
single ``RequestScope`` by the union mount. ``userId`` (extracted from the HTTP path
|
|
101
|
+
by ``HttpHandlers.RequestScope``) flows automatically into ``currentUser`` (looked up
|
|
102
|
+
in the DB by ``UserRepository.RequestScope``) without any glue code.
|
|
103
|
+
|
|
104
|
+
``responseSent`` is an IO resource: it sends the HTTP response as a side effect and
|
|
105
|
+
returns ``None``. The handler body is a single attribute access — all logic lives in
|
|
106
|
+
the DI graph. In an async framework (e.g. FastAPI), return an ``asyncio.Task[None]``
|
|
107
|
+
instead of a coroutine, which cannot be safely awaited in multiple dependents.
|
|
108
|
+
|
|
109
|
+
.. literalinclude:: ../../mixinv2-examples/src/mixinv2_examples/app_decorator/step4_http_server.py
|
|
110
|
+
:language: python
|
|
111
|
+
:start-after: # [docs:step4-http-server]
|
|
112
|
+
:end-before: # [/docs:step4-http-server]
|
|
113
|
+
:dedent:
|
|
114
|
+
|
|
115
|
+
Assemble into a module and evaluate — pass the module directly to ``evaluate()``:
|
|
116
|
+
|
|
117
|
+
.. code-block:: python
|
|
118
|
+
|
|
119
|
+
import mixinv2_examples.app_decorator.step4_http_server as step4_http_server
|
|
120
|
+
|
|
121
|
+
root = evaluate(step4_http_server, modules_public=True).App(
|
|
122
|
+
databasePath="/var/lib/myapp/prod.db",
|
|
123
|
+
host="127.0.0.1",
|
|
124
|
+
port=8080,
|
|
125
|
+
)
|
|
126
|
+
server = root.server
|
|
127
|
+
|
|
128
|
+
Swapping to a test configuration is just different kwargs; no scope or composition changes:
|
|
129
|
+
|
|
130
|
+
.. code-block:: python
|
|
131
|
+
|
|
132
|
+
test_root = evaluate(step4_http_server, modules_public=True).App(
|
|
133
|
+
databasePath=":memory:", # fresh, isolated database for each test
|
|
134
|
+
host="127.0.0.1",
|
|
135
|
+
port=0, # OS assigns a free port
|
|
136
|
+
)
|
|
137
|
+
# test_root.connection → sqlite3.Connection to :memory:
|
|
138
|
+
# test_root.server → HTTPServer on OS-assigned port
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
Decorator reference
|
|
142
|
+
-------------------
|
|
143
|
+
|
|
144
|
+
.. list-table::
|
|
145
|
+
:header-rows: 1
|
|
146
|
+
:widths: 25 75
|
|
147
|
+
|
|
148
|
+
* - Decorator
|
|
149
|
+
- Purpose
|
|
150
|
+
* - ``@scope``
|
|
151
|
+
- Define a DI container (class) or sub-namespace
|
|
152
|
+
* - ``@resource``
|
|
153
|
+
- Declare a lazily-computed value; parameters are injected by name
|
|
154
|
+
* - ``@public``
|
|
155
|
+
- Expose a ``@resource`` or ``@scope`` to external callers
|
|
156
|
+
* - ``@extern``
|
|
157
|
+
- Declare a required dependency that must come from the composed scope
|
|
158
|
+
* - ``@patch``
|
|
159
|
+
- Provide a transformation that wraps an existing resource
|
|
160
|
+
* - ``@patch_many``
|
|
161
|
+
- Like ``@patch`` but yields multiple transformations at once
|
|
162
|
+
* - ``@merge``
|
|
163
|
+
- Define how patches are aggregated (e.g. ``frozenset``, ``list``, custom reducer)
|
|
164
|
+
* - ``@eager``
|
|
165
|
+
- Force evaluation at scope creation rather than on first access
|
|
166
|
+
* - ``@extend(*refs)``
|
|
167
|
+
- Inherit from other scopes explicitly (for package-level union mounts)
|
|
168
|
+
* - ``evaluate(*scopes)``
|
|
169
|
+
- Resolve and union-mount one or more scopes into a single dependency graph
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
Python modules as scopes
|
|
173
|
+
-------------------------
|
|
174
|
+
|
|
175
|
+
The ``@scope`` classes above are a teaching convenience — the real-world style is
|
|
176
|
+
plain Python modules, just like pytest fixtures don't require a class. Every
|
|
177
|
+
``@scope`` class maps directly to a module file; pass it to ``evaluate()`` the same
|
|
178
|
+
way:
|
|
179
|
+
|
|
180
|
+
.. code-block:: python
|
|
181
|
+
|
|
182
|
+
import SqliteDatabase # SqliteDatabase.py with @extern / @resource / @public
|
|
183
|
+
import UserRepository # UserRepository/ package
|
|
184
|
+
|
|
185
|
+
The same decorators work on module-level functions exactly as on class methods. A
|
|
186
|
+
subpackage becomes a nested scope — ``UserRepository/RequestScope/`` is the
|
|
187
|
+
module equivalent of a nested ``@scope class RequestScope``.
|
|
188
|
+
|
|
189
|
+
Use ``@extend`` in a package's ``__init__.py`` to declare the composition, then
|
|
190
|
+
``evaluate()`` receives the single package:
|
|
191
|
+
|
|
192
|
+
.. literalinclude:: ../../mixinv2-examples/src/mixinv2_examples/app_di/__init__.py
|
|
193
|
+
:language: python
|
|
194
|
+
:start-after: # [docs:module-extend]
|
|
195
|
+
:end-before: # [/docs:module-extend]
|
|
196
|
+
|
|
197
|
+
.. code-block:: python
|
|
198
|
+
|
|
199
|
+
import myapp
|
|
200
|
+
|
|
201
|
+
root = evaluate(myapp, modules_public=True).App(databasePath=":memory:")
|
|
202
|
+
|
|
203
|
+
Runnable module-based equivalents of all tutorial examples are in
|
|
204
|
+
:github:`packages/mixinv2-examples/tests/test_readme_package_examples.py`,
|
|
205
|
+
using the fixture package at
|
|
206
|
+
:github:`packages/mixinv2-examples/src/mixinv2_examples/app_di/`.
|
|
@@ -9,7 +9,7 @@ source = "uv-dynamic-versioning"
|
|
|
9
9
|
vcs = "git"
|
|
10
10
|
style = "pep440"
|
|
11
11
|
bump = false
|
|
12
|
-
fallback-version = "0.
|
|
12
|
+
fallback-version = "0.0.0.dev0"
|
|
13
13
|
metadata = false
|
|
14
14
|
|
|
15
15
|
[project]
|
|
@@ -43,4 +43,4 @@ only-include = ["src/mixinv2"]
|
|
|
43
43
|
sources = ["src"]
|
|
44
44
|
|
|
45
45
|
[project.urls]
|
|
46
|
-
Repository = "https://github.com/Atry/
|
|
46
|
+
Repository = "https://github.com/Atry/MIXINv2"
|
|
@@ -217,7 +217,7 @@ def _make_scalar_resource(
|
|
|
217
217
|
) -> EndofunctionMergerDefinition[object]:
|
|
218
218
|
"""Create an EndofunctionMergerDefinition that returns a scalar value.
|
|
219
219
|
|
|
220
|
-
This is the
|
|
220
|
+
This is the .mixin.yaml equivalent of ``@resource def field(): return scalar_value``.
|
|
221
221
|
"""
|
|
222
222
|
return EndofunctionMergerDefinition(
|
|
223
223
|
inherits=(),
|
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
Getting Started with Decorators
|
|
2
|
-
================================
|
|
3
|
-
|
|
4
|
-
The examples below build a single web application step by step, introducing one
|
|
5
|
-
concept at a time. All code is runnable with the standard library only.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Step 1 — Define services
|
|
9
|
-
------------------------
|
|
10
|
-
|
|
11
|
-
Decorate a class with ``@scope`` to make it a DI container. Annotate each value with
|
|
12
|
-
``@resource`` and expose it with ``@public``. Resources declare their dependencies as
|
|
13
|
-
ordinary function parameters; the framework injects them by name.
|
|
14
|
-
|
|
15
|
-
Use ``@extern`` to declare a dependency that must come from outside the scope — the
|
|
16
|
-
equivalent of a pytest fixture parameter. Pass multiple scopes to ``evaluate()`` to
|
|
17
|
-
compose them; dependencies are resolved by name across scope boundaries. Config
|
|
18
|
-
values are passed as kwargs when calling the evaluated scope.
|
|
19
|
-
|
|
20
|
-
.. literalinclude:: ../../tests/test_readme_examples.py
|
|
21
|
-
:language: python
|
|
22
|
-
:start-after: # [docs:step1-define-services]
|
|
23
|
-
:end-before: # [/docs:step1-define-services]
|
|
24
|
-
:dedent:
|
|
25
|
-
|
|
26
|
-
``SQLiteDatabase`` owns ``database_path``; ``UserRepository`` has no knowledge of the
|
|
27
|
-
database layer — it only declares ``connection: sqlite3.Connection`` as a parameter
|
|
28
|
-
and receives it automatically from the composed scope.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Step 2 — Layer cross-cutting concerns with ``@patch`` and ``@merge``
|
|
32
|
-
--------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
``@patch`` wraps an existing resource value with a transformation. This lets an
|
|
35
|
-
add-on scope modify a value without touching the scope that defined it — the same
|
|
36
|
-
idea as pytest's ``monkeypatch``, but composable.
|
|
37
|
-
|
|
38
|
-
.. literalinclude:: ../../tests/test_readme_examples.py
|
|
39
|
-
:language: python
|
|
40
|
-
:start-after: # [docs:step2-patch]
|
|
41
|
-
:end-before: # [/docs:step2-patch]
|
|
42
|
-
:dedent:
|
|
43
|
-
|
|
44
|
-
When several independent scopes each contribute a piece to the same resource, use
|
|
45
|
-
``@merge`` to define how the contributions are aggregated:
|
|
46
|
-
|
|
47
|
-
.. literalinclude:: ../../tests/test_readme_examples.py
|
|
48
|
-
:language: python
|
|
49
|
-
:start-after: # [docs:step2-merge]
|
|
50
|
-
:end-before: # [/docs:step2-merge]
|
|
51
|
-
:dedent:
|
|
52
|
-
|
|
53
|
-
A ``@patch`` can itself declare ``@extern`` dependencies, which are injected like any
|
|
54
|
-
other resource:
|
|
55
|
-
|
|
56
|
-
.. literalinclude:: ../../tests/test_readme_examples.py
|
|
57
|
-
:language: python
|
|
58
|
-
:start-after: # [docs:step2-patch-extern]
|
|
59
|
-
:end-before: # [/docs:step2-patch-extern]
|
|
60
|
-
:dedent:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
Step 3 — Force evaluation at startup with ``@eager``
|
|
64
|
-
-----------------------------------------------------
|
|
65
|
-
|
|
66
|
-
All resources are lazy by default: computed on first access, then cached for the
|
|
67
|
-
lifetime of the scope. Mark a resource ``@eager`` to evaluate it immediately when
|
|
68
|
-
``evaluate()`` returns — useful for schema migrations or connection pre-warming that
|
|
69
|
-
must complete before the application starts serving requests:
|
|
70
|
-
|
|
71
|
-
.. literalinclude:: ../../tests/test_readme_examples.py
|
|
72
|
-
:language: python
|
|
73
|
-
:start-after: # [docs:step3-eager]
|
|
74
|
-
:end-before: # [/docs:step3-eager]
|
|
75
|
-
:dedent:
|
|
76
|
-
|
|
77
|
-
Without ``@eager``, the ``CREATE TABLE`` would not run until ``root.connection`` is first
|
|
78
|
-
accessed.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
Step 4 — App scope vs request scope
|
|
82
|
-
------------------------------------
|
|
83
|
-
|
|
84
|
-
So far all resources have had application lifetime: created once at startup and
|
|
85
|
-
reused for every request. Real applications also need per-request resources — values
|
|
86
|
-
that must be created fresh for each incoming request and discarded when it completes.
|
|
87
|
-
|
|
88
|
-
A nested ``@scope`` named ``RequestScope`` serves as a per-request factory. The
|
|
89
|
-
framework injects it by name as a ``Callable``; calling
|
|
90
|
-
``RequestScope(request=handler)`` returns a fresh instance.
|
|
91
|
-
|
|
92
|
-
The application below has four scopes, each owning only its own concern:
|
|
93
|
-
|
|
94
|
-
- **SQLiteDatabase** — owns ``database_path``, provides ``connection``
|
|
95
|
-
- **UserRepository** — business logic; owns ``user_count`` and per-request ``current_user``
|
|
96
|
-
- **HttpHandlers** — HTTP layer; owns per-request ``user_id``, ``response_body``, ``response_sent``
|
|
97
|
-
- **NetworkServer** — network layer; owns ``host``/``port``, creates the ``HTTPServer``
|
|
98
|
-
|
|
99
|
-
``UserRepository.RequestScope`` and ``HttpHandlers.RequestScope`` are composed into a
|
|
100
|
-
single ``RequestScope`` by the union mount. ``user_id`` (extracted from the HTTP path
|
|
101
|
-
by ``HttpHandlers.RequestScope``) flows automatically into ``current_user`` (looked up
|
|
102
|
-
in the DB by ``UserRepository.RequestScope``) without any glue code.
|
|
103
|
-
|
|
104
|
-
``response_sent`` is an IO resource: it sends the HTTP response as a side effect and
|
|
105
|
-
returns ``None``. The handler body is a single attribute access — all logic lives in
|
|
106
|
-
the DI graph. In an async framework (e.g. FastAPI), return an ``asyncio.Task[None]``
|
|
107
|
-
instead of a coroutine, which cannot be safely awaited in multiple dependents.
|
|
108
|
-
|
|
109
|
-
.. code-block:: python
|
|
110
|
-
|
|
111
|
-
import threading
|
|
112
|
-
import urllib.request
|
|
113
|
-
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
114
|
-
from types import ModuleType
|
|
115
|
-
|
|
116
|
-
from mixinv2 import LexicalReference, extend
|
|
117
|
-
|
|
118
|
-
@scope
|
|
119
|
-
class SQLiteDatabase:
|
|
120
|
-
@extern
|
|
121
|
-
def database_path() -> str: ... # database owns its own config
|
|
122
|
-
|
|
123
|
-
# App-scoped: one connection for the entire process lifetime.
|
|
124
|
-
# check_same_thread=False: created in main thread, used in handler threads.
|
|
125
|
-
@public
|
|
126
|
-
@resource
|
|
127
|
-
def connection(database_path: str) -> sqlite3.Connection:
|
|
128
|
-
db = sqlite3.connect(database_path, check_same_thread=False)
|
|
129
|
-
db.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
|
|
130
|
-
db.execute("INSERT INTO users VALUES (1, 'alice')")
|
|
131
|
-
db.execute("INSERT INTO users VALUES (2, 'bob')")
|
|
132
|
-
db.commit()
|
|
133
|
-
return db
|
|
134
|
-
|
|
135
|
-
@scope
|
|
136
|
-
class UserRepository:
|
|
137
|
-
@extern
|
|
138
|
-
def connection() -> sqlite3.Connection: ...
|
|
139
|
-
|
|
140
|
-
# @scope as a composable dataclass — fields are @extern, constructed via DI.
|
|
141
|
-
@public
|
|
142
|
-
@scope
|
|
143
|
-
class User:
|
|
144
|
-
@public
|
|
145
|
-
@extern
|
|
146
|
-
def user_id() -> int: ...
|
|
147
|
-
|
|
148
|
-
@public
|
|
149
|
-
@extern
|
|
150
|
-
def name() -> str: ...
|
|
151
|
-
|
|
152
|
-
# App-scoped: total count, computed once.
|
|
153
|
-
@public
|
|
154
|
-
@resource
|
|
155
|
-
def user_count(connection: sqlite3.Connection) -> int:
|
|
156
|
-
(count,) = connection.execute("SELECT COUNT(*) FROM users").fetchone()
|
|
157
|
-
return count
|
|
158
|
-
|
|
159
|
-
# Request-scoped: per-request DB resources.
|
|
160
|
-
@public
|
|
161
|
-
@scope
|
|
162
|
-
class RequestScope:
|
|
163
|
-
@extern
|
|
164
|
-
def user_id() -> int: ... # provided by HttpHandlers.RequestScope
|
|
165
|
-
|
|
166
|
-
@public
|
|
167
|
-
@resource
|
|
168
|
-
def current_user(
|
|
169
|
-
connection: sqlite3.Connection, user_id: int, User: Callable
|
|
170
|
-
) -> object:
|
|
171
|
-
row = connection.execute(
|
|
172
|
-
"SELECT id, name FROM users WHERE id = ?", (user_id,)
|
|
173
|
-
).fetchone()
|
|
174
|
-
assert row is not None, f"no user with id={user_id}"
|
|
175
|
-
identifier, name = row
|
|
176
|
-
return User(user_id=identifier, name=name)
|
|
177
|
-
|
|
178
|
-
@scope
|
|
179
|
-
class HttpHandlers:
|
|
180
|
-
# RequestScope is nested because its lifetime is per-request,
|
|
181
|
-
# not per-application.
|
|
182
|
-
@public
|
|
183
|
-
@scope
|
|
184
|
-
class RequestScope:
|
|
185
|
-
@extern
|
|
186
|
-
def request() -> BaseHTTPRequestHandler: ...
|
|
187
|
-
|
|
188
|
-
# user_id is extracted from the request and injected into
|
|
189
|
-
# UserRepository.RequestScope.current_user automatically.
|
|
190
|
-
@public
|
|
191
|
-
@resource
|
|
192
|
-
def user_id(request: BaseHTTPRequestHandler) -> int:
|
|
193
|
-
return int(request.path.split("/")[-1])
|
|
194
|
-
|
|
195
|
-
# current_user and user_count resolved from their respective scopes.
|
|
196
|
-
@public
|
|
197
|
-
@resource
|
|
198
|
-
def response_body(user_count: int, current_user: object) -> bytes:
|
|
199
|
-
return f"total={user_count} current={current_user.name}".encode()
|
|
200
|
-
|
|
201
|
-
# IO resource: sends the HTTP response as a side effect.
|
|
202
|
-
@public
|
|
203
|
-
@resource
|
|
204
|
-
def response_sent(
|
|
205
|
-
request: BaseHTTPRequestHandler,
|
|
206
|
-
response_body: bytes,
|
|
207
|
-
) -> None:
|
|
208
|
-
request.send_response(200)
|
|
209
|
-
request.end_headers()
|
|
210
|
-
request.wfile.write(response_body)
|
|
211
|
-
|
|
212
|
-
@scope
|
|
213
|
-
class NetworkServer:
|
|
214
|
-
@extern
|
|
215
|
-
def host() -> str: ... # network layer owns its own config
|
|
216
|
-
|
|
217
|
-
@extern
|
|
218
|
-
def port() -> int: ...
|
|
219
|
-
|
|
220
|
-
# RequestScope is injected by name as a Callable (StaticScope).
|
|
221
|
-
# Calling RequestScope(request=handler) returns a fresh InstanceScope.
|
|
222
|
-
@public
|
|
223
|
-
@resource
|
|
224
|
-
def server(host: str, port: int, RequestScope: Callable) -> HTTPServer:
|
|
225
|
-
class Handler(BaseHTTPRequestHandler):
|
|
226
|
-
def do_GET(self) -> None:
|
|
227
|
-
RequestScope(request=self).response_sent
|
|
228
|
-
|
|
229
|
-
return HTTPServer((host, port), Handler)
|
|
230
|
-
|
|
231
|
-
# Declare composition via @extend — each scope only knows its own config.
|
|
232
|
-
@extend(
|
|
233
|
-
LexicalReference(path=("SQLiteDatabase",)),
|
|
234
|
-
LexicalReference(path=("UserRepository",)),
|
|
235
|
-
LexicalReference(path=("HttpHandlers",)),
|
|
236
|
-
LexicalReference(path=("NetworkServer",)),
|
|
237
|
-
)
|
|
238
|
-
@public
|
|
239
|
-
@scope
|
|
240
|
-
class app:
|
|
241
|
-
pass
|
|
242
|
-
|
|
243
|
-
# Assemble into a module and evaluate — composition is declared above, not here.
|
|
244
|
-
myapp = ModuleType("myapp")
|
|
245
|
-
myapp.SQLiteDatabase = SQLiteDatabase
|
|
246
|
-
myapp.UserRepository = UserRepository
|
|
247
|
-
myapp.HttpHandlers = HttpHandlers
|
|
248
|
-
myapp.NetworkServer = NetworkServer
|
|
249
|
-
myapp.app = app
|
|
250
|
-
|
|
251
|
-
root = evaluate(myapp, modules_public=True).app(
|
|
252
|
-
database_path="/var/lib/myapp/prod.db",
|
|
253
|
-
host="127.0.0.1",
|
|
254
|
-
port=8080,
|
|
255
|
-
)
|
|
256
|
-
server = root.server
|
|
257
|
-
|
|
258
|
-
Swapping to a test configuration is just different kwargs; no scope or composition changes:
|
|
259
|
-
|
|
260
|
-
.. code-block:: python
|
|
261
|
-
|
|
262
|
-
test_root = evaluate(myapp, modules_public=True).app(
|
|
263
|
-
database_path=":memory:", # fresh, isolated database for each test
|
|
264
|
-
host="127.0.0.1",
|
|
265
|
-
port=0, # OS assigns a free port
|
|
266
|
-
)
|
|
267
|
-
# test_root.connection → sqlite3.Connection to :memory:
|
|
268
|
-
# test_root.server → HTTPServer on OS-assigned port
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
Decorator reference
|
|
272
|
-
-------------------
|
|
273
|
-
|
|
274
|
-
.. list-table::
|
|
275
|
-
:header-rows: 1
|
|
276
|
-
:widths: 25 75
|
|
277
|
-
|
|
278
|
-
* - Decorator
|
|
279
|
-
- Purpose
|
|
280
|
-
* - ``@scope``
|
|
281
|
-
- Define a DI container (class) or sub-namespace
|
|
282
|
-
* - ``@resource``
|
|
283
|
-
- Declare a lazily-computed value; parameters are injected by name
|
|
284
|
-
* - ``@public``
|
|
285
|
-
- Expose a ``@resource`` or ``@scope`` to external callers
|
|
286
|
-
* - ``@extern``
|
|
287
|
-
- Declare a required dependency that must come from the composed scope
|
|
288
|
-
* - ``@patch``
|
|
289
|
-
- Provide a transformation that wraps an existing resource
|
|
290
|
-
* - ``@patch_many``
|
|
291
|
-
- Like ``@patch`` but yields multiple transformations at once
|
|
292
|
-
* - ``@merge``
|
|
293
|
-
- Define how patches are aggregated (e.g. ``frozenset``, ``list``, custom reducer)
|
|
294
|
-
* - ``@eager``
|
|
295
|
-
- Force evaluation at scope creation rather than on first access
|
|
296
|
-
* - ``@extend(*refs)``
|
|
297
|
-
- Inherit from other scopes explicitly (for package-level union mounts)
|
|
298
|
-
* - ``evaluate(*scopes)``
|
|
299
|
-
- Resolve and union-mount one or more scopes into a single dependency graph
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
Python modules as scopes
|
|
303
|
-
-------------------------
|
|
304
|
-
|
|
305
|
-
The ``@scope`` classes above are a teaching convenience — the real-world style is
|
|
306
|
-
plain Python modules, just like pytest fixtures don't require a class. Every
|
|
307
|
-
``@scope`` class maps directly to a module file; pass it to ``evaluate()`` the same
|
|
308
|
-
way:
|
|
309
|
-
|
|
310
|
-
.. code-block:: python
|
|
311
|
-
|
|
312
|
-
import sqlite_database # sqlite_database.py with @extern / @resource / @public
|
|
313
|
-
import user_repository # user_repository/ package
|
|
314
|
-
|
|
315
|
-
The same decorators work on module-level functions exactly as on class methods. A
|
|
316
|
-
subpackage becomes a nested scope — ``user_repository/request_scope/`` is the
|
|
317
|
-
module equivalent of a nested ``@scope class RequestScope``.
|
|
318
|
-
|
|
319
|
-
Use ``@extend`` in a package's ``__init__.py`` to declare the composition, then
|
|
320
|
-
``evaluate()`` receives the single package:
|
|
321
|
-
|
|
322
|
-
.. literalinclude:: ../../tests/fixtures/app_di/__init__.py
|
|
323
|
-
:language: python
|
|
324
|
-
:start-after: # [docs:module-extend]
|
|
325
|
-
:end-before: # [/docs:module-extend]
|
|
326
|
-
|
|
327
|
-
.. code-block:: python
|
|
328
|
-
|
|
329
|
-
import myapp
|
|
330
|
-
|
|
331
|
-
root = evaluate(myapp, modules_public=True).app(database_path=":memory:")
|
|
332
|
-
|
|
333
|
-
Runnable module-based equivalents of all tutorial examples are in
|
|
334
|
-
:github:`tests/test_readme_package_examples.py`,
|
|
335
|
-
using the fixture package at
|
|
336
|
-
:github:`tests/fixtures/app_di/`.
|
|
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
|