Skip to content

Commit 2e5427b

Browse files
authored
Support for mkdocs material (#26)
* Support for mkdocs material superfences syntax Add a block info parsing function that detects and parse when it uses the pymdown superfences brace format. To use this parser, use `--markdown-docs-syntax=superfences`
1 parent 2038e06 commit 2e5427b

File tree

3 files changed

+135
-27
lines changed

3 files changed

+135
-27
lines changed

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,20 @@ assert a + " world" == "hello world"
136136
```
137137
````
138138

139+
### Compatibility with Material for MkDocs
140+
141+
Material for Mkdocs is not compatible with the default syntax.
142+
143+
But if the extension `pymdownx.superfences` is configured for mkdocs, the brace format can be used:
144+
````markdown
145+
```{.python continuation}
146+
````
147+
148+
You will need to call pytest with the `--markdown-docs-syntax` option:
149+
```shell
150+
pytest --markdown-docs --markdown-docs-syntax=superfences
151+
```
152+
139153
## MDX Comments for Metadata Options
140154
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:
141155

@@ -175,4 +189,4 @@ Or for fun, you can use this plugin to include testing of the validity of snippe
175189
* Line numbers are "wrong" for docstring-inlined snippets (since we don't know where in the file the docstring starts)
176190
* Line numbers are "wrong" for continuation blocks even in pure markdown files (can be worked out with some refactoring)
177191
* There are probably more appropriate ways to use pytest internal APIs to get more features "for free" - current state of the code is a bit "patch it til' it works".
178-
* Assertions are not rewritten w/ pretty data structure inspection like they are with regular pytest tests by default
192+
* Assertions are not rewritten w/ pretty data structure inspection like they are with regular pytest tests by default

src/pytest_markdown_docs/plugin.py

Lines changed: 79 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pathlib
66
import pytest
77
import typing
8+
from enum import Enum
89

910
from _pytest._code import ExceptionInfo
1011
from _pytest.config.argparsing import Parser
@@ -25,6 +26,11 @@
2526
MARKER_NAME = "markdown-docs"
2627

2728

29+
class FenceSyntax(Enum):
30+
default = "default"
31+
superfences = "superfences"
32+
33+
2834
class MarkdownInlinePythonItem(pytest.Item):
2935
def __init__(
3036
self,
@@ -155,6 +161,7 @@ def reportinfo(self):
155161
def extract_code_blocks(
156162
markdown_string: str,
157163
markdown_type: str = "md",
164+
fence_syntax: FenceSyntax = FenceSyntax.default,
158165
) -> typing.Generator[typing.Tuple[str, typing.List[str], int], None, None]:
159166
import markdown_it
160167

@@ -167,7 +174,10 @@ def extract_code_blocks(
167174
continue
168175

169176
startline = block.map[0] + 1 # skip the info line when counting
170-
code_info = block.info.split()
177+
if fence_syntax == FenceSyntax.superfences:
178+
code_info = parse_superfences_block_info(block.info)
179+
else:
180+
code_info = block.info.split()
171181

172182
lang = code_info[0] if code_info else None
173183
code_options = set(code_info) - {lang}
@@ -200,6 +210,34 @@ def extract_code_blocks(
200210
prev = code_block
201211

202212

213+
def parse_superfences_block_info(block_info: str) -> typing.List[str]:
214+
"""Parse PyMdown Superfences block info syntax.
215+
216+
The default `python continuation` format is not compatible with Material for Mkdocs.
217+
But, PyMdown Superfences has a special brace format to add options to code fence blocks: `{.<lang> <option1> <option2>}`.
218+
219+
This function also works if the default syntax is used to allow for mixed usage.
220+
"""
221+
block_info = block_info.strip()
222+
223+
if not block_info.startswith("{"):
224+
# default syntax
225+
return block_info.split()
226+
227+
block_info = block_info.strip("{}")
228+
code_info = block_info.split()
229+
# Lang may not be the first but is always the first element that starts with a dot.
230+
# (https://facelessuser.github.io/pymdown-extensions/extensions/superfences/#injecting-classes-ids-and-attributes)
231+
dot_lang = next(
232+
(info_part for info_part in code_info if info_part.startswith(".")), None
233+
)
234+
if dot_lang:
235+
code_info.remove(dot_lang)
236+
lang = dot_lang[1:]
237+
code_info.insert(0, lang)
238+
return code_info
239+
240+
203241
def is_mdx_comment(block: "Token") -> bool:
204242
return (
205243
block.type == "inline"
@@ -219,29 +257,6 @@ def extract_options_from_mdx_comment(comment: str) -> typing.Set[str]:
219257
return set(option.strip() for option in comment.split(" ") if option)
220258

221259

222-
def find_object_tests_recursive(
223-
module_name: str, object: typing.Any
224-
) -> typing.Generator[
225-
typing.Tuple[int, typing.Any, typing.Tuple[str, typing.List[str], int]], None, None
226-
]:
227-
docstr = inspect.getdoc(object)
228-
229-
if docstr:
230-
for i, code_block in enumerate(extract_code_blocks(docstr)):
231-
yield i, object, code_block
232-
233-
for member_name, member in inspect.getmembers(object):
234-
if member_name.startswith("_"):
235-
continue
236-
237-
if (
238-
inspect.isclass(member)
239-
or inspect.isfunction(member)
240-
or inspect.ismethod(member)
241-
) and member.__module__ == module_name:
242-
yield from find_object_tests_recursive(module_name, member)
243-
244-
245260
class MarkdownDocstringCodeModule(pytest.Module):
246261
def collect(self):
247262
if pytest.version_tuple >= (8, 1, 0):
@@ -257,7 +272,7 @@ def collect(self):
257272
test_code,
258273
fixture_names,
259274
start_line,
260-
) in find_object_tests_recursive(module.__name__, module):
275+
) in self.find_object_tests_recursive(module.__name__, module):
261276
obj_name = (
262277
getattr(obj, "__qualname__", None)
263278
or getattr(obj, "__name__", None)
@@ -272,14 +287,44 @@ def collect(self):
272287
fake_line_numbers=True, # TODO: figure out where docstrings are in file to offset line numbers properly
273288
)
274289

290+
def find_object_tests_recursive(
291+
self, module_name: str, object: typing.Any
292+
) -> typing.Generator[
293+
typing.Tuple[int, typing.Any, typing.Tuple[str, typing.List[str], int]],
294+
None,
295+
None,
296+
]:
297+
docstr = inspect.getdoc(object)
298+
299+
if docstr:
300+
fence_syntax = FenceSyntax(self.config.option.markdowndocs_syntax)
301+
for i, code_block in enumerate(
302+
extract_code_blocks(docstr, fence_syntax=fence_syntax)
303+
):
304+
yield i, object, code_block
305+
306+
for member_name, member in inspect.getmembers(object):
307+
if member_name.startswith("_"):
308+
continue
309+
310+
if (
311+
inspect.isclass(member)
312+
or inspect.isfunction(member)
313+
or inspect.ismethod(member)
314+
) and member.__module__ == module_name:
315+
yield from self.find_object_tests_recursive(module_name, member)
316+
275317

276318
class MarkdownTextFile(pytest.File):
277319
def collect(self):
278320
markdown_content = self.path.read_text("utf8")
321+
fence_syntax = FenceSyntax(self.config.option.markdowndocs_syntax)
279322

280323
for i, (code_block, fixture_names, start_line) in enumerate(
281324
extract_code_blocks(
282-
markdown_content, markdown_type=self.path.suffix.replace(".", "")
325+
markdown_content,
326+
markdown_type=self.path.suffix.replace(".", ""),
327+
fence_syntax=fence_syntax,
283328
)
284329
):
285330
yield MarkdownInlinePythonItem.from_parent(
@@ -321,6 +366,14 @@ def pytest_addoption(parser: Parser) -> None:
321366
help="run ",
322367
dest="markdowndocs",
323368
)
369+
group.addoption(
370+
"--markdown-docs-syntax",
371+
action="store",
372+
choices=[choice.value for choice in FenceSyntax],
373+
default="default",
374+
help="Choose an alternative fences syntax",
375+
dest="markdowndocs_syntax",
376+
)
324377

325378

326379
def pytest_addhooks(pluginmanager):

tests/plugin_test.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,3 +350,44 @@ def test_notest_mdx_comment(testdir):
350350
)
351351
result = testdir.runpytest("--markdown-docs")
352352
result.assert_outcomes(passed=0)
353+
354+
355+
def test_superfences_format_markdown(testdir):
356+
testdir.makefile(
357+
".md",
358+
"""
359+
```python
360+
b = "hello"
361+
```
362+
363+
```{.python continuation}
364+
assert b + " world" == "hello world"
365+
```
366+
367+
# the lang may not be the first element
368+
```{other_option .python .other-class continuation}
369+
assert b + " world" == "hello world"
370+
```
371+
""",
372+
)
373+
result = testdir.runpytest("--markdown-docs", "--markdown-docs-syntax=superfences")
374+
result.assert_outcomes(passed=3)
375+
376+
377+
def test_superfences_format_docstring(testdir):
378+
testdir.makepyfile(
379+
"""
380+
def simple():
381+
\"\"\"
382+
```python
383+
b = "hello"
384+
```
385+
386+
```{.python continuation}
387+
assert b + " world" == "hello world"
388+
```
389+
\"\"\"
390+
"""
391+
)
392+
result = testdir.runpytest("--markdown-docs", "--markdown-docs-syntax=superfences")
393+
result.assert_outcomes(passed=2)

0 commit comments

Comments
 (0)