Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

= AsciiDoctest: Verifiable, Stateful, and Interactive Documentation with AsciiDoc
:toc: left
:idprefix:
:idseparator: -

image:https://github.com/webmaven/asciidoctest/actions/workflows/ci.yml/badge.svg[CI Status, link=https://github.com/webmaven/asciidoctest/actions/workflows/ci.yml]
image:https://img.shields.io/pypi/v/asciidoctest.svg[PyPI Version, link=https://pypi.org/project/asciidoctest/]
image:https://img.shields.io/badge/python-3.14-blue.svg[Python Version, link=https://www.python.org/]
image:https://img.shields.io/badge/coverage-97%25-green.svg[Test Coverage]
image:https://img.shields.io/badge/License-Apache_2.0-blue.svg[License, link=https://opensource.org/licenses/Apache-2.0]

link:https://github.com/webmaven/asciidoctest[AsciiDoctest] is an executable documentation runner and narrative testing tool designed to parse, collect, and verify link:https://www.python.org/[Python] code blocks directly from link:https://asciidoc.org/[AsciiDoc] (`.adoc`) files and Python docstrings. It is published on link:https://pypi.org/project/asciidoctest/[PyPI].

It integrates AST-based parsing using link:https://github.com/webmaven/asciidoctrine[asciidoctrine] and link:https://github.com/webmaven/asciidocstring[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 link:https://github.com/webmaven/asciidoctrine[asciidoctrine] and link:https://github.com/webmaven/asciidocstring[asciidocstring].
* Fully honors standard link:https://asciidoc.org/[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 link:https://github.com/webmaven/asciidoctrine[asciidoctrine] (AsciiDoc parser) and link:https://github.com/webmaven/asciidocstring[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 link:https://docs.pytest.org/[pytest] collectors.
* **Unittest Compatibility**: Suite wrappers (`DocTestSuite` and `DocFileSuite`) designed to integrate with the standard library link:https://docs.python.org/3/library/unittest.html[unittest] runner.

== Installation

[source,bash]
----
pip install asciidoctest
----

== Pytest Integration

AsciiDoctest automatically registers as a link:https://docs.pytest.org/[pytest] plugin. Simply execute `pytest` in your project directory:

[source,bash]
----
pytest
----

=== Configuration

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

[source,ini]
----
[pytest]
asciidoctest_mode = eager
----

Alternatively, you can supply the `--asciidoctest-mode` flag:

[source,bash]
----
pytest --asciidoctest-mode=eager
----

== Unittest Integration

To use the standard library link:https://docs.python.org/3/library/unittest.html[unittest] package, load tests using `DocFileSuite` or `DocTestSuite`:

[source,python]
----
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:

[source,python]
----
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)
----

== 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:

[source,python,shared]
----
>>> 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]`:

[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:

[source,python,test]
----
>>> 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]`:

[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:

[source,python,shared]
----
>>> 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:

[source,python,shared="db_context"]
----
>>> db_conn = {"status": "connected", "database": "users"}
>>> db_conn["status"]
'connected'
----

[source,python,shared="cache_context"]
----
>>> cache = {"user:1": "Alice"}
>>> "db_conn" in dir() or "db_conn" in locals()
False
----

[source,python,shared="db_context"]
----
>>> 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:

[source,python,reset,shared]
----
>>> "x" in dir() or "x" in locals()
False
>>> x = "fresh_start"
>>> x
'fresh_start'
----

== Contributing

We welcome contributions to link:https://github.com/webmaven/asciidoctest[AsciiDoctest]! Please review our link:https://github.com/webmaven/asciidoctest/blob/main/SECURITY.adoc[Security Policy] before running untrusted code blocks during development.

AsciiDoctest maintains strict quality standards:

* Fully annotated link:https://www.python.org/[Python] types checked via link:https://mypy-lang.org/[mypy].
* Code formatting and linting verified with link:https://github.com/astral-sh/ruff[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:
+
[source,bash]
----
$ 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:
+
[source,bash]
----
$ pytest
----

3. Check code coverage reports:
+
[source,bash]
----
$ 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:

link:https://www.apache.org/licenses/LICENSE-2.0[https://www.apache.org/licenses/LICENSE-2.0]

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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

asciidoctest-0.2.0a4.tar.gz (35.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

asciidoctest-0.2.0a4-py3-none-any.whl (20.2 kB view details)

Uploaded Python 3

File details

Details for the file asciidoctest-0.2.0a4.tar.gz.

File metadata

  • Download URL: asciidoctest-0.2.0a4.tar.gz
  • Upload date:
  • Size: 35.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.5

File hashes

Hashes for asciidoctest-0.2.0a4.tar.gz
Algorithm Hash digest
SHA256 3bc1bb0d9036d61d85e61fc464e65bc7f7bf605ae95a76e2788345bf3b623a56
MD5 2bc2ec65cccf9ec52689b5003312aaa7
BLAKE2b-256 a8678b42f79196d2408d5d755e010252b57fb6d046738aa8408d4dd39f943d7c

See more details on using hashes here.

File details

Details for the file asciidoctest-0.2.0a4-py3-none-any.whl.

File metadata

  • Download URL: asciidoctest-0.2.0a4-py3-none-any.whl
  • Upload date:
  • Size: 20.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.5

File hashes

Hashes for asciidoctest-0.2.0a4-py3-none-any.whl
Algorithm Hash digest
SHA256 d007a9f26b9332c8b01d6bb4a4c61f8ad50fc00e040b4e9e1dd3def8b9883853
MD5 c06158fc6b57a6a42874c31a43d467b0
BLAKE2b-256 7122427a6cea3b25f7dc3496d37379b5c936641b4cf96aa22f47c6ca9c16f9d0

See more details on using hashes here.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page