Skip to content

Commit 0abe69e

Browse files
authored
test: run the writing-integrations examples in test-docs (#652)
1 parent 163e10f commit 0abe69e

3 files changed

Lines changed: 64 additions & 9 deletions

File tree

‎conftest.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import ast
99
import asyncio
1010
import inspect
11+
import pathlib
1112
from collections.abc import Iterator
1213

1314
import pytest
@@ -23,10 +24,11 @@
2324
_EXCLUDED_PAGES = (
2425
"adr/*",
2526
"agents/*",
26-
"integrations/*",
2727
"migration/*",
2828
)
2929

30+
_FRAMEWORK_INDEPENDENT_INTEGRATION_PAGES = frozenset({"writing-integrations.md"})
31+
3032
_EXPECTED_ERROR_KEY = "__docs_expected_error__"
3133

3234
_RAISES_LEXER = DirectiveInHTMLCommentLexer("raises", arguments=r"\w+")
@@ -65,7 +67,7 @@ def _evaluate_python(example: Example) -> None:
6567
pytest.fail(f"expected the block to raise {expected_error}, it raised nothing")
6668

6769

68-
pytest_collect_file = Sybil(
70+
_collect_docs = Sybil(
6971
parsers=[
7072
CodeBlockParser("python", _evaluate_python),
7173
SkipParser(),
@@ -76,3 +78,9 @@ def _evaluate_python(example: Example) -> None:
7678
patterns=["*.md"],
7779
excludes=_EXCLUDED_PAGES,
7880
).pytest()
81+
82+
83+
def pytest_collect_file(file_path: pathlib.Path, parent: pytest.Collector) -> pytest.Collector | None:
84+
if file_path.parent.name == "integrations" and file_path.name not in _FRAMEWORK_INDEPENDENT_INTEGRATION_PAGES:
85+
return None
86+
return _collect_docs(file_path, parent)

‎docs/agents/docs-style.md‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ not ban the English words: an example class may be a `UserService`, and an appli
6262
## Code samples run
6363

6464
`just test-docs` runs every `python` block in the site, page by page, with each page's blocks
65-
sharing one namespace in order. `integrations/` and `migration/` are excluded in `conftest.py`. A
66-
block that can't run as written gets `<!-- skip: next "reason" -->` on the line above its fence.
67-
Setup the reader doesn't need to see goes in an `<!-- invisible-code-block: python -->` comment. A
68-
block that demonstrates an error gets `<!-- raises: ErrorClassName -->` right above its fence, after
69-
any hidden setup, and passes only if it raises that class. An import of a third-party package that
70-
isn't installed here skips only if the package is listed in `conftest.py`; any other missing import
71-
fails.
65+
sharing one namespace in order. `conftest.py` excludes `migration/` and every framework page under
66+
`integrations/`; `writing-integrations.md` needs no framework and runs. A block that can't run as
67+
written gets `<!-- skip: next "reason" -->` on the line above its fence. Setup the reader doesn't
68+
need to see goes in an `<!-- invisible-code-block: python -->` comment. A block that demonstrates an
69+
error gets `<!-- raises: ErrorClassName -->` right above its fence, after any hidden setup, and
70+
passes only if it raises that class. An import of a third-party package that isn't installed here
71+
skips only if the package is listed in `conftest.py`; any other missing import fails.

‎docs/integrations/writing-integrations.md‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,41 @@ child container should open at. This is the single source of the
3131
kind → scope mapping; `setup_di` registers them and the child-container builder
3232
dispatches off them.
3333

34+
<!-- invisible-code-block: python
35+
import dataclasses
36+
import types
37+
import typing
38+
39+
from modern_di import Container, Group, Scope, providers
40+
41+
42+
class _App:
43+
def __init__(self) -> None:
44+
self.state = types.SimpleNamespace()
45+
46+
47+
class _Request:
48+
def __init__(self, app: _App) -> None:
49+
self.app = app
50+
51+
52+
class _WebSocket:
53+
def __init__(self, app: _App) -> None:
54+
self.app = app
55+
56+
57+
myfw = types.SimpleNamespace(App=_App, Request=_Request, WebSocket=_WebSocket, Depends=lambda dependency: dependency)
58+
HTTPConnection = _Request
59+
T_co = typing.TypeVar("T_co", covariant=True)
60+
61+
62+
class MyService: ...
63+
64+
65+
class Dependencies(Group):
66+
service = providers.Factory(MyService, scope=Scope.REQUEST)
67+
-->
68+
3469
```python
3570
from modern_di import Scope, providers
3671

@@ -168,6 +203,18 @@ def FromDI(dependency: providers.AbstractProvider[T_co] | type[T_co]) -> T_co:
168203
return typing.cast(T_co, myfw.Depends(Dependency(integrations.Marker(dependency))))
169204
```
170205

206+
<!-- invisible-code-block: python
207+
app = myfw.App()
208+
setup_di(app, Container(groups=[Dependencies]))
209+
request = myfw.Request(app)
210+
builder = build_di_container(request)
211+
request_container = await anext(builder)
212+
assert request_container.scope is Scope.REQUEST
213+
assert request_container.resolve(myfw.Request) is request
214+
assert isinstance(await FromDI(Dependencies.service)(request_container), MyService)
215+
await builder.aclose()
216+
-->
217+
171218
`Marker.resolve(container)` is a single call, invariant across every
172219
integration and both modes: it hands the wrapped provider-or-type to
173220
`container.resolve_dependency`, which routes to `resolve_provider`/`resolve`

0 commit comments

Comments
 (0)