Skip to content

Commit

Permalink
Add support for specifying metadata options in MDX comments (#29)
Browse files Browse the repository at this point in the history
* Add support for specifying metadata options in MDX comments
* Make space the delimiter for metadata options
* Only look for MDX comments when extracting from .mdx files
  • Loading branch information
bunchesofdonald authored Oct 21, 2024
1 parent bd1617c commit 8fdfe93
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 6 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,25 @@ assert a + " world" == "hello world"
```
````

## MDX Comments for Metadata Options
In .mdx files, you can use MDX comments to provide additional options for code blocks. These comments should be placed immediately before the code block and take the following form:

```mdx
{/* pmd-metadata: notest fixture:capsys */}
```python
print("hello")
captured = capsys.readouterr()
assert captured.out == "hello\n"
```

The following options can be specified using MDX comments:

* notest: Exclude the code block from testing.
* fixture:<name>: Apply named pytest fixtures to the code block.
* continuation: Continue from the previous code block, allowing you to carry over state.

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

## Testing of this plugin

You can test this module itself (sadly not using markdown tests at the moment) using pytest:
Expand Down
50 changes: 44 additions & 6 deletions src/pytest_markdown_docs/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
from _pytest.fixtures import FixtureRequest as TopRequest # type: ignore


if typing.TYPE_CHECKING:
from markdown_it.token import Token

MARKER_NAME = "markdown-docs"


Expand Down Expand Up @@ -151,36 +154,71 @@ def reportinfo(self):

def extract_code_blocks(
markdown_string: str,
markdown_type: str = "md",
) -> typing.Generator[typing.Tuple[str, typing.List[str], int], None, None]:
import markdown_it

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

prev = ""
for block in tokens:
for i, block in enumerate(tokens):
if block.type != "fence" or not block.map:
continue

startline = block.map[0] + 1 # skip the info line when counting
code_info = block.info.split()

lang = code_info[0] if code_info else None

if lang in ("py", "python", "python3") and "notest" not in code_info:
code_options = set(code_info) - {lang}

if markdown_type == "mdx":
# In MDX, comments are enclosed within a paragraph block and must be
# placed directly above the corresponding code fence. The token
# sequence is as follows:
# i-3: paragraph_open
# i-2: comment
# i-1: paragraph_close
# i: code fence
#
# Therefore, to retrieve the MDX comment associated with the current
# code fence (at index `i`), we need to access the token at `i - 2`.
if i >= 2 and is_mdx_comment(tokens[i - 2]):
code_options |= extract_options_from_mdx_comment(tokens[i - 2].content)

if lang in ("py", "python", "python3") and "notest" not in code_options:
code_block = block.content

if "continuation" in code_info:
if "continuation" in code_options:
code_block = prev + code_block
startline = -1 # this disables proper line numbers, TODO: adjust line numbers *per snippet*

fixture_names = [
f[len("fixture:") :] for f in code_info if f.startswith("fixture:")
f[len("fixture:") :] for f in code_options if f.startswith("fixture:")
]
yield code_block, fixture_names, startline
prev = code_block


def is_mdx_comment(block: "Token") -> bool:
return (
block.type == "inline"
and block.content.strip().startswith("{/*")
and block.content.strip().endswith("*/}")
and "pmd-metadata:" in block.content
)


def extract_options_from_mdx_comment(comment: str) -> typing.Set[str]:
comment = (
comment.strip()
.replace("{/*", "")
.replace("*/}", "")
.replace("pmd-metadata:", "")
)
return set(option.strip() for option in comment.split(" ") if option)


def find_object_tests_recursive(
module_name: str, object_name: str, object: typing.Any
) -> typing.Generator[typing.Tuple[str, typing.List[str], int], None, None]:
Expand Down Expand Up @@ -230,7 +268,7 @@ def collect(self):
markdown_content = self.path.read_text("utf8")

for code_block, fixture_names, start_line in extract_code_blocks(
markdown_content
markdown_content, markdown_type=self.path.suffix.replace(".", "")
):
yield MarkdownInlinePythonItem.from_parent(
self,
Expand Down
91 changes: 91 additions & 0 deletions tests/plugin_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,94 @@ def some_global():
)
result = testdir.runpytest("--markdown-docs")
result.assert_outcomes(passed=2)


def test_continuation_mdx_comment(testdir):
testdir.makefile(
".mdx",
"""
```python
b = "hello"
```
{/* pmd-metadata: continuation */}
```python
assert b + " world" == "hello world"
```
""",
)
result = testdir.runpytest("--markdown-docs")
result.assert_outcomes(passed=2)


def test_specific_fixture_mdx_comment(testdir):
testdir.makeconftest(
"""
import pytest
@pytest.fixture()
def initialize_specific():
import pytest_markdown_docs
pytest_markdown_docs.bump = getattr(pytest_markdown_docs, "bump", 0) + 1
yield "foobar"
pytest_markdown_docs.bump -= 1
"""
)

testdir.makefile(
".mdx",
"""
{/* pmd-metadata: fixture:initialize_specific */}
```python
import pytest_markdown_docs
assert pytest_markdown_docs.bump == 1
assert initialize_specific == "foobar"
```
""",
)
result = testdir.runpytest("--markdown-docs")
result.assert_outcomes(passed=1)


def test_multiple_fixtures_mdx_comment(testdir):
testdir.makeconftest(
"""
import pytest
@pytest.fixture()
def initialize_specific():
import pytest_markdown_docs
pytest_markdown_docs.bump = getattr(pytest_markdown_docs, "bump", 0) + 1
yield "foobar"
pytest_markdown_docs.bump -= 1
@pytest.fixture
def another_fixture():
return "hello"
"""
)

testdir.makefile(
".mdx",
"""
{/* pmd-metadata: fixture:initialize_specific fixture:another_fixture */}
```python
import pytest_markdown_docs
assert pytest_markdown_docs.bump == 1
assert initialize_specific == "foobar"
```
""",
)
result = testdir.runpytest("--markdown-docs")
result.assert_outcomes(passed=1)


def test_notest_mdx_comment(testdir):
testdir.makefile(
".mdx",
"""
{/* pmd-metadata: notest */}
```python
assert True
```
""",
)
result = testdir.runpytest("--markdown-docs")
result.assert_outcomes(passed=0)

0 comments on commit 8fdfe93

Please sign in to comment.