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