Skip to content

Repository files navigation

AsciiDoctest: Verifiable, Stateful, and Interactive Documentation with AsciiDoc

CI Status PyPI Version Python Version Test Coverage License

AsciiDoctest is an executable documentation runner and narrative testing tool designed to parse, collect, and verify Python code blocks directly from AsciiDoc (.adoc) files and Python docstrings. It is published on PyPI.

It integrates AST-based parsing using asciidoctrine and asciidocstring to provide accurate, standard-compliant testing of your documentation’s code examples.

Why AsciiDoctest?

Traditional doctest extractors rely on fragile regular expressions to parse block structures and find code examples. This approach frequently breaks when encountering inline styles, unparsed attributes, nested lists, or complex block boundaries.

AsciiDoctest solves this by relying on native Abstract Syntax Tree (AST) parsing:

  • Leverages full structural documents parsed via asciidoctrine and asciidocstring.

  • Fully honors standard AsciiDoc roles, positional arguments, attributes, and includes.

  • Maintains high-fidelity source coordinates (line and column mappings) for precise failure reporting.

Executable Documentation State Models

Writing a multi-step tutorial, interactive guide, or documentation book often requires code examples that build on top of each other. At the same time, writers need the ability to run isolated one-off checks or explore alternative paths without leaking state.

AsciiDoctest provides a unified, symmetric state model across both interactive REPL blocks and non-interactive script blocks to facilitate predictable, stateful documentation.

A Python code block is considered marked when it contains the test, shared, or reset keyword inside its block header—either as a positional argument (e.g., [source,python,test]), an explicit attribute (e.g., [source,python,test="true"]), or a block role (e.g., [source,python,role="shared"]).

Based on these markers, blocks are executed under the following models:

  • No Marker or Attribute: Treated as an ordinary, non-executable code listing.

    ~~ Exception: In eager mode, unmarked listings are executed as test blocks (fully isolated and ephemeral) ONLY if no block in the entire document has any explicit markers of either sort. ~~

  • test (Isolated & Ephemeral): Runs in a completely clean, isolated namespace ({}). Any variables or state changes created during its execution are immediately discarded. This aligns with standard unit-testing isolation principles.

  • shared (Read-Write & Persistent): Participates in a continuous, stateful document timeline (like a stateful notebook). Any classes, variables, or functions defined in early shared blocks are fully accessible and modifiable in subsequent shared blocks.

  • shared, test (Ephemeral Copy): Gets access to a read-only copy of the shared state at that point in the document, but any mutations or local bindings created within the block are discarded when the block completes.

  • Named Contexts ([source,python,shared="context_name"]): Maintains separate, persistent state timelines. Blocks specifying the same shared="<name>" attribute share state with each other, completely independent of the default shared timeline.

  • Explicit Reset ([source,python,reset]): Clears accumulated shared state and named contexts, providing a fresh execution environment from that block forward.

  • Section Boundary Scoping: In multi-section documents, moving across top-level section boundaries (== Section One to == Section Two) automatically resets the default shared namespace and named contexts. This prevents state leakage between unrelated classes, functions, or narrative topics while preserving sequential flow within each section.

  • Tolerant Illustrative Includes: By default, include directives (include::…​[]) are ignored during doctest extraction (preprocess_directives=False), allowing illustrative code references in documentation without requiring referenced files to exist on disk.

This model is extremely consistent, easy to reason about, and ensures that your documentation’s code examples are always accurate and tested.

Features

  • AST-Based Parsing: Structural parsing using asciidoctrine (AsciiDoc parser) and asciidocstring (AsciiDoc docstring parser).

  • Execution Modes:

    • explicit (default): Only executes blocks with explicit markers (test, shared, or reset).

    • eager: Falls back to executing all [source,python] listings as isolated test blocks, but only if the document contains zero explicit markers.

  • Section Scoping & Context Management:

    • Top-level section boundary resets (==) isolating document sections.

    • Named context scopes ([source,python,shared="context_name"]) for parallel shared state timelines.

    • Explicit reset markers ([source,python,reset]) for manual state resets.

  • Direct Python & Docstring Extraction: Programmatic API (extract_and_run_docstring_tests) to extract and run doctests from .py files, directories, or loaded modules with per-symbol scope isolation.

  • Tolerant Include Handling: Illustrative includes are safely skipped during AST extraction (preprocess_directives=False), preventing missing file errors for documentation-only references.

  • Pytest Integration: Automatic discovery and execution of .adoc files and Python docstrings via registered pytest collectors.

  • Unittest Compatibility: Suite wrappers (DocTestSuite and DocFileSuite) designed to integrate with the standard library unittest runner.

Installation

pip install asciidoctest

Pytest Integration

AsciiDoctest automatically registers as a pytest plugin. Simply execute pytest in your project directory:

pytest

Configuration

You can configure collection behavior in your pyproject.toml or pytest.ini:

[pytest]
asciidoctest_mode = eager
asciidoctest_split_sections = true

Alternatively, you can supply command-line flags:

pytest --asciidoctest-mode=eager --asciidoctest-split-sections

When --asciidoctest-split-sections is enabled, AsciiDoctest collects each test block as an individual pytest item named after its section title and 1-based block index (for example, README.adoc::Getting_Started::asciidoctest_block_1), enabling targeted execution via pytest -k <Pattern>.

Unittest Integration

To use the standard library unittest package, load tests using DocFileSuite or DocTestSuite:

import unittest
from asciidoctest import DocFileSuite

def suite():
    return DocFileSuite("README.adoc")

if __name__ == "__main__":
    unittest.main(defaultTest="suite")

Direct Docstring Extraction API

In addition to pytest and unittest runners, AsciiDoctest provides a direct programmatic API extract_and_run_docstring_tests to discover and run docstring doctests directly on files, directories, or module objects:

from asciidoctest import extract_and_run_docstring_tests

# Run doctests on a single Python source file
stats = extract_and_run_docstring_tests("src/mypackage/utils.py")
print(stats)  # => {'total': 3, 'passed': 3, 'failed': 0}

# Run doctests across an entire directory tree
stats = extract_and_run_docstring_tests("src/mypackage/")

# Run doctests directly on an imported module
import mypackage
stats = extract_and_run_docstring_tests(mypackage)

Testing in Pyodide & WebAssembly

AsciiDoctest and its core dependencies are pure Python and can run in WebAssembly environments like Pyodide.

Coexistence with pytest-pyodide

AsciiDoctest test items (AsciiDocItem and DocstringTestItem) initialize _fixtureinfo for compatibility with pytest-pyodide. You can install both plugins in the same pytest test environment without runner crashes or attribute errors.

Testing Strategies for Pyodide Packages

When documenting and testing frontend or browser-focused Python packages intended for Pyodide:

  1. Pure Pyodide / In-Wasm Execution: Install asciidoctest into the Pyodide runtime (via micropip.install("asciidoctest")) and execute tests directly inside Node.js or browser Pyodide using DocFileSuite, DocTestSuite, or extract_and_run_docstring_tests.

  2. Host Pytest Wrapper Bridge: When orchestrating browser tests from host pytest using pytest-pyodide (@pytest.mark.driver), parse document blocks on the host using asciidoctest.parser.parse_adoc_tests(content) and dispatch block contents into the browser VM using driver helpers such as selenium.run_python(block.content).

  3. Dual-Target Fast Feedback: Use lightweight mocks for browser globals (js, pyodide.ffi) during host pytest runs for instant local validation, paired with browser integration runs in CI.

State Management (shared & reset) in Pyodide

Frontend and browser-based Python applications are naturally stateful. AsciiDoctest’s execution markers map directly to WebAssembly workflows:

  • Stateful Tutorials ([source,python,shared]): Re-initializing WebAssembly modules, opening IndexedDB handles, or creating canvas contexts on every 2-line snippet is prohibitively slow. Using shared lets you build realistic, multi-step browser tutorials where connection objects and state carry sequentially from block to block.

  • Parallel Named Scopes (shared="<name>"): Isolate distinct browser components or concurrent workers in the same document without collision.

  • Explicit & Section Boundaries (reset and == Section): When transitioning between tutorial chapters or independent widgets, use [source,python,reset] or top-level section headings to clear Python shared state.

  • Python State vs. DOM Teardown: Note that while reset and == Section automatically clear Python’s internal namespace dictionaries, host JavaScript artifacts (such as DOM nodes attached to document.body or event listeners added to window) persist in the browser engine unless explicitly removed in Python code or test fixtures.

Examples

Below are standard test blocks demonstrating interactive and script-based execution.

Interactive Session

We can run interactive Python sessions with expected outputs. We mark this block with [source,python,shared] to allow this interactive block’s setup state (x) to be persisted for subsequent blocks:

>>> x = "asciidoctest"
>>> x.upper()
'ASCIIDOCTEST'

Sequential Script Block

We can run script-based test blocks with standard Python assertions. Since they share the same continuous narrative namespace, variables defined in previous [source,python,shared] blocks are accessible. We mark this block with [source,python,shared]:

assert x == "asciidoctest"
y = len(x)
assert y == 12

Directives Support

Standard doctest directives such as ELLIPSIS are supported. We mark this block with [source,python,test] so that it is independent and runs with a clean, isolated namespace:

>>> print(x)
Traceback (most recent call last):
    ...
NameError: name 'x' is not defined
>>> x = "Hello, beautiful world!"
>>> print(x)
Hello, ... world!

Ephemeral Copy of Shared State

We can grant a test block access to an ephemeral copy of the accumulated shared state at that point in the document. Any modifications made within the block are discarded afterwards. We mark this block with [source,python,shared,test]:

>>> print(x)
asciidoctest
>>> x = "Hello, beautiful world!"
>>> print(x)
Hello, ... world!

To demonstrate that the changes in the shared, test block were indeed discarded and did not modify the persistent shared namespace, a subsequent [source,python,shared] block shows that x remains unchanged:

>>> print(x)
asciidoctest

Named Context Scopes

You can maintain multiple distinct, persistent timelines using named context scopes. For example, database and cache states can evolve independently:

>>> db_conn = {"status": "connected", "database": "users"}
>>> db_conn["status"]
'connected'
>>> cache = {"user:1": "Alice"}
>>> "db_conn" in dir() or "db_conn" in locals()
False
>>> db_conn["database"]
'users'
>>> "cache" in dir() or "cache" in locals()
False

Explicit Reset Markers

You can explicitly reset the shared state at any point using the reset marker:

>>> "x" in dir() or "x" in locals()
False
>>> x = "fresh_start"
>>> x
'fresh_start'

Contributing

We welcome contributions to AsciiDoctest! Please review our Security Policy before running untrusted code blocks during development.

AsciiDoctest maintains strict quality standards:

  • Fully annotated Python types checked via mypy.

  • Code formatting and linting verified with Ruff.

  • Comprehensive unit and integration test coverage kept above 95%.

To get started on development locally:

  1. Clone the repository and set up the virtual environment:

    $ git clone https://github.com/webmaven/asciidoctest.git
    $ cd asciidoctest
    $ python3 -m venv .venv
    $ source .venv/bin/activate
    $ pip install -e ".[test,docs]"
  2. Run the complete test suite:

    $ pytest
  3. Check code coverage reports:

    $ coverage run -m pytest
    $ coverage report -m

License

Licensed under the Apache License, Version 2.0 (the "License"). You may obtain a copy of the License at:

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

About

AsciiDoctest is a Python doctest runner designed to parse, collect, and execute tests from AsciiDoc (.adoc) files and Python AsciiDoc docstrings.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages