Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,21 @@ The following options can be specified using MDX comments:

This approach allows you to add metadata to the code block without modifying the code fence itself, making it particularly useful in MDX environments.

## Customizing your own custom MarkdownIt parser

You can configure your own [Markdown-it-py](https://pypi.org/project/markdown-it-py/) parser used by `pytest-markdown-docs` by defining a `pytest_markdown_docs_markdown_it`. For example, you can support
`mkdocs`'s admonitions with:

```python
def pytest_markdown_docs_markdown_it():
import markdown_it
from mdit_py_plugins.admon import admon_plugin

mi = markdown_it.MarkdownIt(config="commonmark")
mi.use(admon_plugin)
return mi
```

## Testing of this plugin

You can test this module itself (sadly not using markdown tests at the moment) using pytest:
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ dev-dependencies = [
"pre-commit>=3.5.0",
"pytest~=8.1.0",
"ruff~=0.9.10",
"mdit-py-plugins~=0.4.2"
]
7 changes: 7 additions & 0 deletions src/pytest_markdown_docs/hooks.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import typing

if typing.TYPE_CHECKING:
from markdown_it import MarkdownIt


def pytest_markdown_docs_globals() -> typing.Dict[str, typing.Any]:
return {}


def pytest_markdown_docs_markdown_it() -> "MarkdownIt":
"""Configure a custom markdown_it.MarkdownIt parser."""
18 changes: 16 additions & 2 deletions src/pytest_markdown_docs/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

if typing.TYPE_CHECKING:
from markdown_it.token import Token
from markdown_it import MarkdownIt

logger = logging.getLogger("pytest-markdown-docs")

Expand Down Expand Up @@ -123,15 +124,20 @@ def get_prefixed_strings(


def extract_fence_tests(
markdown_it_parsers: typing.List["MarkdownIt"],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be missing something here, but why is this a list? (seems like we are throwing away anything except the first element anyway)

@thomasjpfan thomasjpfan Apr 8, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pytest returns a list because the hook can be defined multiple times. For example:

tests/
    conftest.py <- hook can be defined here
    io/
        conftest.py <- same hook can be defined here
        my_test.py

The default is to have a list that contains all the hooks and the nested conftest.py gets called first so they are first in the hooks list.


I simplified the code by using @pytest.hookspec(firstresult=True), which returns the first one by default: 0be6a10

Also, 5fe7f6f, which defines a default MarkdownIt parser using the hook.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, yeah this makes sense! Thanks for the explanation and I like the firstresult and default implementation

markdown_string: str,
start_line_offset: int,
source_path: pathlib.Path,
markdown_type: str = "md",
fence_syntax: FenceSyntax = FenceSyntax.default,
) -> typing.Generator[FenceTestDefinition, None, None]:
import markdown_it
if not markdown_it_parsers:
from markdown_it import MarkdownIt

mi = MarkdownIt(config="commonmark")
else:
mi = markdown_it_parsers[0]

mi = markdown_it.MarkdownIt(config="commonmark")
tokens = mi.parse(markdown_string)

prev = ""
Expand Down Expand Up @@ -294,8 +300,13 @@ def find_object_tests_recursive(
or "<Unnamed obj>"
)
fence_syntax = FenceSyntax(self.config.option.markdowndocs_syntax)
markdown_it_parsers = (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the list comment above - maybe it's better to validate the hook return value here (making sure it's at most one specification, since that's what we support) and then just pass in the single MarkdownIt we want to use?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With 0be6a10 and 5fe7f6f, this should be a single MarkdownIt object.

The remaining user error is returning a non-MarkdownIt object:

def pytest_markdown_docs_markdown_it():
    return "abc"

Do you think it's worth checking for this edge case?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the other changes I don't think we need to check for anything here 👍

self.config.hook.pytest_markdown_docs_markdown_it()
)

for i, fence_test in enumerate(
extract_fence_tests(
markdown_it_parsers,
docstr,
docstring_offset,
source_path=self.path,
Expand All @@ -317,8 +328,11 @@ def collect(self):
markdown_content = self.path.read_text("utf8")
fence_syntax = FenceSyntax(self.config.option.markdowndocs_syntax)

markdown_it_parsers = self.config.hook.pytest_markdown_docs_markdown_it()

for i, fence_test in enumerate(
extract_fence_tests(
markdown_it_parsers,
markdown_content,
source_path=self.path,
start_line_offset=0,
Expand Down
40 changes: 40 additions & 0 deletions tests/plugin_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,3 +470,43 @@ def runtest(self, test, args):
],
consecutive=True,
)


def test_admonition_markdown_text_file(testdir):
testdir.makeconftest(
"""
def pytest_markdown_docs_globals():
return {"a": "hello"}

def pytest_markdown_docs_markdown_it():
import markdown_it
from mdit_py_plugins.admon import admon_plugin

mi = markdown_it.MarkdownIt(config="commonmark")
mi.use(admon_plugin)
return mi
"""
)

testdir.makefile(
".md",
"""
??? quote

```python
assert a + " world" == "hello world"
```

!!! info
```python
assert False
```

???+ note
```python
**@ # this is a syntax error
```
""",
)
result = testdir.runpytest("--markdown-docs")
result.assert_outcomes(passed=1, failed=2)