mixinv2 0.3.0.post12.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.
Files changed (21) hide show
  1. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/PKG-INFO +1 -1
  2. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/docs/Makefile +1 -5
  3. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/docs/conf.py +20 -1
  4. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/docs/mixinv2-tutorial.rst +51 -51
  5. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/docs/specification.md +26 -26
  6. mixinv2-0.3.0.post24.dev0/docs/tutorial.rst +206 -0
  7. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/src/mixinv2/_mixin_parser.py +1 -1
  8. mixinv2-0.3.0.post12.dev0/docs/tutorial.rst +0 -336
  9. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/.gitignore +0 -0
  10. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/README.md +0 -0
  11. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/docs/_static/favicon.svg +0 -0
  12. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/docs/_static/logo.svg +0 -0
  13. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/docs/index.rst +0 -0
  14. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/docs/installation.rst +0 -0
  15. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/pyproject.toml +0 -0
  16. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/src/mixinv2/__init__.py +0 -0
  17. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/src/mixinv2/_config.py +0 -0
  18. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/src/mixinv2/_core.py +0 -0
  19. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/src/mixinv2/_interned_linked_list.py +0 -0
  20. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/src/mixinv2/_mixin_directory.py +0 -0
  21. {mixinv2-0.3.0.post12.dev0 → mixinv2-0.3.0.post24.dev0}/src/mixinv2/_runtime.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mixinv2
3
- Version: 0.3.0.post12.dev0
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
5
  Project-URL: Repository, https://github.com/Atry/MIXINv2
6
6
  Author-email: "Yang, Bo" <yang-bo@yang-bo.com>
@@ -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 api-docs
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/mixinv2
@@ -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', 'api/mixinv2.rst']
42
+ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'api/modules.rst']
41
43
 
42
44
 
43
45
 
@@ -72,3 +74,20 @@ extlinks = {
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
- - **``.oyaml`` files** contain all application logic, written in MIXINv2. MIXINv2 is not just a configuration format — it is a
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 ``.oyaml`` is **portable**: it is decoupled from the
34
- Python FFI layer. Swap the FFI adapters and the same ``.oyaml`` logic runs against
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:`tests/fixtures/app_oyaml/stdlib_ffi/FFI.py`)
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:: ../../tests/fixtures/app_oyaml/stdlib_ffi/FFI/SqliteConnectAndExecuteScript.py
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:: ../../tests/fixtures/app_oyaml/stdlib_ffi/FFI/GetItem.py
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:: ../../tests/fixtures/app_oyaml/stdlib_ffi/FFI/HttpSendResponse.py
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 ``.oyaml``.
68
+ in the ``.mixin.yaml``.
69
69
 
70
70
 
71
- ``.oyaml`` composition
72
- ----------------------
71
+ ``.mixin.yaml`` composition
72
+ ---------------------------
73
73
 
74
- An ``.oyaml`` file describes a **dependency graph**, not an execution sequence.
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.oyaml``, which references FFI adapters
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 (``stdlib_ffi/FFI.py``) overrides these slots at composition time. This
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 ``.oyaml`` files need no changes.
82
+ different FFI implementation and the ``.mixin.yaml`` files need no changes.
83
83
 
84
- The following sections walk through ``Library.oyaml`` one scope at a time,
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:: ../../tests/fixtures/app_oyaml/Library.oyaml
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
- - **``field: []``** — an **extern declaration**, the ``.oyaml`` equivalent of
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
- - **``- [FFI, SqliteConnectAndExecuteScript]``** — **inheritance**. ``_db`` inherits
100
+ - ``- [FFI, SqliteConnectAndExecuteScript]`` — **inheritance**. ``_db`` inherits
101
101
  the FFI adapter, gaining all of its resources (``connection``).
102
- - **``database_path: [database_path]``** — **wiring**. The reference ``[database_path]``
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
- - **``connection: [_db, connection]``** — **path navigation**. Access the
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:: ../../tests/fixtures/app_oyaml/Library.oyaml
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
- - **``User:``** is a **nested scope** with two extern fields — the ``.oyaml``
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
- - **``connection: []``** declares that ``UserRepository`` expects a ``connection``
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:: ../../tests/fixtures/app_oyaml/Library.oyaml
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 ``.oyaml`` directly defines ``current_user`` as a scope with two fields. 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:: ../../tests/fixtures/app_oyaml/Library.oyaml
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: ``[RequestScope, ~, written]``** — instead of declaring
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:: ../../tests/fixtures/app_oyaml/Library.oyaml
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.oyaml``. They reference ``[FFI, Xxx]`` which
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.oyaml`` — integration entry point
194
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
193
+ ``Apps.mixin.yaml`` — integration entry point
194
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
195
195
 
196
- ``Apps.oyaml`` is a separate file that inherits the real FFI implementation and
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:: ../../tests/fixtures/app_oyaml/Apps.oyaml
199
+ .. literalinclude:: ../../mixinv2-examples/src/mixinv2_examples/app_mixin/Apps.mixin.yaml
200
200
  :language: yaml
201
- :start-after: # [docs:apps-oyaml]
202
- :end-before: # [/docs:apps-oyaml]
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.oyaml`` file needs
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.oyaml`` runs unchanged on both synchronous and asynchronous
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 <tests/fixtures/app_oyaml/async_ffi/FFI/>`) — the business
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.oyaml`` itself is *colorless*.
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.oyaml``. This is not four
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 (``.oyaml``)
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 ``.oyaml`` file, portable across FFI
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.app_oyaml as app_oyaml
313
+ import tests.fixtures.app_mixin as app_mixin
314
314
  from mixinv2 import evaluate
315
315
 
316
- # evaluate() auto-discovers stdlib_ffi/, Library.oyaml, and Apps.oyaml.
317
- root = evaluate(app_oyaml, modules_public=True)
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 .oyaml file name.
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.oyaml`` — the Python
332
- FFI adapters and ``Library.oyaml`` never change. Swapping the FFI layer is just a
333
- different ``@scope`` module — the ``.oyaml`` business logic never changes.
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:`tests/fixtures/app_oyaml/`.
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.oyaml
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.oyaml
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.oyaml`, the `example_calculation` overlay uses the `add` operation to add two numbers:
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.oyaml
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.oyaml
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.oyaml
468
- │ ├── electric.oyaml
469
- │ └── car.oyaml
467
+ │ ├── vehicle.mixin.yaml
468
+ │ ├── electric.mixin.yaml
469
+ │ └── car.mixin.yaml
470
470
 
471
471
  ├── config/
472
- │ └── settings.oyaml
472
+ │ └── settings.mixin.yaml
473
473
  └── test/
474
- └── test_car.oyaml
474
+ └── test_car.mixin.yaml
475
475
  ```
476
476
 
477
- **vehicle.oyaml**:
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.oyaml**:
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.oyaml**:
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.oyaml**:
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.oyaml
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.oyaml` inherits `Car` and `Electric` from the `module` directory using the format `[module, Car]` and `[module, Electric, battery_capacity]`.
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.oyaml
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.oyaml
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.oyaml
743
+ # number.mixin.yaml
744
744
  Number:
745
745
  - {} # Represents a number type overlay
746
746
 
747
- # value.oyaml
747
+ # value.mixin.yaml
748
748
  value_42:
749
749
  - 42 # Defines scalar value 42
750
750
 
751
- # my_number.oyaml
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.oyaml
773
+ # person.mixin.yaml
774
774
  PersonDetails:
775
775
  name: [String]
776
776
  age: [Number]
777
777
 
778
- # height.oyaml
778
+ # height.mixin.yaml
779
779
  height_value: 180 # Scalar value representing height
780
780
 
781
- # combined_person.oyaml
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.oyaml
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.oyaml
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/`.
@@ -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 oyaml equivalent of ``@resource def field(): return scalar_value``.
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/`.