@@ -225,6 +225,45 @@ type
225225
226226### Doctest Guidelines
227227
228+ ** All functions and methods MUST have working doctests.** Doctests serve as both documentation and tests.
229+
230+ ** CRITICAL RULES:**
231+ - Doctests MUST actually execute - never comment out ` asyncio.run() ` or similar calls
232+ - Doctests MUST NOT be converted to ` .. code-block:: ` as a workaround (code-blocks don't run)
233+ - If you cannot create a working doctest, ** STOP and ask for help**
234+
235+ ** Available tools for doctests:**
236+ - ` doctest_namespace ` fixtures: ` tmp_path ` , ` asyncio `
237+ - Ellipsis for variable output: ` # doctest: +ELLIPSIS `
238+ - Update ` conftest.py ` to add new fixtures to ` doctest_namespace `
239+
240+ ** ` # doctest: +SKIP ` is NOT permitted** - it's just another workaround that doesn't test anything. Use the fixtures properly.
241+
242+ ** Async doctest pattern:**
243+ ``` python
244+ >> > async def example ():
245+ ... result = await some_async_function()
246+ ... return result
247+ >> > asyncio.run(example())
248+ ' expected output'
249+ ```
250+
251+ ** Using fixtures in doctests:**
252+ ``` python
253+ >> > from pathlib import Path
254+ >> > doc_path = tmp_path / " example.rst" # tmp_path from doctest_namespace
255+ >> > doc_path.write_text(" >>> 1 + 1\\ n2" )
256+ ...
257+ ```
258+
259+ ** When output varies, use ellipsis:**
260+ ``` python
261+ >> > import doctest_docutils
262+ >> > doctest_docutils.__file__ # doctest: +ELLIPSIS
263+ ' .../doctest_docutils.py'
264+ ```
265+
266+ ** Additional guidelines:**
2282671 . ** Use narrative descriptions** for test sections rather than inline comments
2292682 . ** Move complex examples** to dedicated test files at ` tests/examples/<path_to_module>/test_<example>.py `
2302693 . ** Keep doctests simple and focused** on demonstrating usage
@@ -290,6 +329,119 @@ When stuck in debugging loops:
2903293 . ** Document the issue** comprehensively for a fresh approach
2913304 . ** Format for portability** (using quadruple backticks)
292331
332+ ## Asyncio Development
333+
334+ ### Async Subprocess Patterns
335+
336+ ** Always use ` communicate() ` for subprocess I/O:**
337+ ``` python
338+ proc = await asyncio.create_subprocess_shell(... )
339+ stdout, stderr = await proc.communicate() # Prevents deadlocks
340+ ```
341+
342+ ** Use ` asyncio.timeout() ` for timeouts:**
343+ ``` python
344+ async with asyncio.timeout(300 ):
345+ stdout, stderr = await proc.communicate()
346+ ```
347+
348+ ** Handle BrokenPipeError gracefully:**
349+ ``` python
350+ try :
351+ proc.stdin.write(data)
352+ await proc.stdin.drain()
353+ except BrokenPipeError :
354+ pass # Process already exited - expected behavior
355+ ```
356+
357+ ### Async API Conventions
358+
359+ - ** Class naming** : Use ` Async ` prefix: ` AsyncDocTestRunner `
360+ - ** Callbacks** : Async APIs accept only async callbacks (no union types)
361+ - ** Shared logic** : Extract argument-building to sync functions, share with async
362+
363+ ``` python
364+ # Shared argument building (sync)
365+ def build_test_args (verbose : bool = False ) -> dict[str , t.Any]:
366+ args = {" verbose" : verbose}
367+ return args
368+
369+ # Async method uses shared logic
370+ async def run_tests (self , verbose : bool = False ) -> TestResults:
371+ args = build_test_args(verbose)
372+ return await self ._run(** args)
373+ ```
374+
375+ ### Async Testing
376+
377+ ** pytest configuration:**
378+ ``` toml
379+ [tool .pytest .ini_options ]
380+ asyncio_mode = " strict"
381+ asyncio_default_fixture_loop_scope = " function"
382+ ```
383+
384+ ** Async fixture pattern:**
385+ ``` python
386+ @pytest_asyncio.fixture (loop_scope = " function" )
387+ async def async_doc_runner (tmp_path : Path) -> t.AsyncGenerator[AsyncDocTestRunner, None ]:
388+ runner = AsyncDocTestRunner(path = tmp_path)
389+ yield runner
390+ ```
391+
392+ ** Parametrized async tests:**
393+ ``` python
394+ class DocTestFixture (t .NamedTuple ):
395+ test_id: str
396+ doc_content: str
397+ expected: list[str ]
398+
399+ DOC_FIXTURES = [
400+ DocTestFixture(" basic" , " >>> 1 + 1\n 2" , [" pass" ]),
401+ DocTestFixture(" failure" , " >>> 1 + 1\n 3" , [" fail" ]),
402+ ]
403+
404+ @pytest.mark.parametrize (
405+ list (DocTestFixture._fields),
406+ DOC_FIXTURES ,
407+ ids = [f.test_id for f in DOC_FIXTURES ],
408+ )
409+ @pytest.mark.asyncio
410+ async def test_doctest (test_id : str , doc_content : str , expected : list ) -> None :
411+ ...
412+ ```
413+
414+ ### Async Anti-Patterns
415+
416+ ** DON'T poll returncode:**
417+ ``` python
418+ # WRONG
419+ while proc.returncode is None :
420+ await asyncio.sleep(0.1 )
421+
422+ # RIGHT
423+ await proc.wait()
424+ ```
425+
426+ ** DON'T mix blocking calls in async code:**
427+ ``` python
428+ # WRONG
429+ async def bad ():
430+ subprocess.run([" python" , " -m" , " doctest" , file ]) # Blocks event loop!
431+
432+ # RIGHT
433+ async def good ():
434+ proc = await asyncio.create_subprocess_shell(... )
435+ await proc.wait()
436+ ```
437+
438+ ** DON'T close the event loop in tests:**
439+ ``` python
440+ # WRONG - breaks pytest-asyncio cleanup
441+ loop = asyncio.get_running_loop()
442+ loop.close()
443+ ```
444+
293445## Sphinx/Docutils-Specific Considerations
294446
295447### Directive Registration
0 commit comments