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.
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.
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
eagermode, unmarked listings are executed astestblocks (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 earlysharedblocks are fully accessible and modifiable in subsequentsharedblocks. -
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 sameshared="<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 Oneto== 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.
-
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, orreset). -
eager: Falls back to executing all[source,python]listings as isolatedtestblocks, 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.pyfiles, 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
.adocfiles and Python docstrings via registered pytest collectors. -
Unittest Compatibility: Suite wrappers (
DocTestSuiteandDocFileSuite) designed to integrate with the standard library unittest runner.
AsciiDoctest automatically registers as a pytest plugin. Simply execute pytest in your project directory:
pytestYou can configure collection behavior in your pyproject.toml or pytest.ini:
[pytest]
asciidoctest_mode = eager
asciidoctest_split_sections = trueAlternatively, you can supply command-line flags:
pytest --asciidoctest-mode=eager --asciidoctest-split-sectionsWhen --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>.
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")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)AsciiDoctest and its core dependencies are pure Python and can run in WebAssembly environments like 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.
When documenting and testing frontend or browser-focused Python packages intended for Pyodide:
-
Pure Pyodide / In-Wasm Execution: Install
asciidoctestinto the Pyodide runtime (viamicropip.install("asciidoctest")) and execute tests directly inside Node.js or browser Pyodide usingDocFileSuite,DocTestSuite, orextract_and_run_docstring_tests. -
Host Pytest Wrapper Bridge: When orchestrating browser tests from host pytest using
pytest-pyodide(@pytest.mark.driver), parse document blocks on the host usingasciidoctest.parser.parse_adoc_tests(content)and dispatch block contents into the browser VM using driver helpers such asselenium.run_python(block.content). -
Dual-Target Fast Feedback: Use lightweight mocks for browser globals (
js,pyodide.ffi) during hostpytestruns for instant local validation, paired with browser integration runs in CI.
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. Usingsharedlets 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 (
resetand== 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
resetand== Sectionautomatically clear Python’s internal namespace dictionaries, host JavaScript artifacts (such as DOM nodes attached todocument.bodyor event listeners added towindow) persist in the browser engine unless explicitly removed in Python code or test fixtures.
Below are standard test blocks demonstrating interactive and script-based execution.
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'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 == 12Standard 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!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)
asciidoctestYou 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()
FalseWe welcome contributions to AsciiDoctest! Please review our Security Policy before running untrusted code blocks during development.
AsciiDoctest maintains strict quality standards:
To get started on development locally:
-
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]"
-
Run the complete test suite:
$ pytest
-
Check code coverage reports:
$ coverage run -m pytest $ coverage report -m
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.