pytest-markdown-docs 0.6.0__tar.gz → 0.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. pytest_markdown_docs-0.7.1/Makefile +8 -0
  2. pytest_markdown_docs-0.6.0/README.md → pytest_markdown_docs-0.7.1/PKG-INFO +27 -1
  3. pytest_markdown_docs-0.6.0/PKG-INFO → pytest_markdown_docs-0.7.1/README.md +15 -14
  4. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/pyproject.toml +1 -3
  5. pytest_markdown_docs-0.7.1/pytest-markdown-docs.iml +13 -0
  6. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/src/pytest_markdown_docs/plugin.py +179 -85
  7. pytest_markdown_docs-0.7.1/tests/conftest.py +10 -0
  8. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/tests/plugin_test.py +84 -3
  9. pytest_markdown_docs-0.7.1/tests/support/docstring_error_after.py +11 -0
  10. pytest_markdown_docs-0.7.1/tests/support/docstring_error_before.py +11 -0
  11. pytest_markdown_docs-0.6.0/poetry.lock +0 -410
  12. pytest_markdown_docs-0.6.0/poetry.toml +0 -3
  13. pytest_markdown_docs-0.6.0/tests/conftest.py +0 -1
  14. pytest_markdown_docs-0.6.0/uv.lock +0 -356
  15. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/.github/pull_request_template.md +0 -0
  16. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/.github/workflows/check.yml +0 -0
  17. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/.github/workflows/ci.yml +0 -0
  18. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/.github/workflows/codeql.yml +0 -0
  19. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/.gitignore +0 -0
  20. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/.pre-commit-config.yaml +0 -0
  21. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/LICENSE +0 -0
  22. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/src/pytest_markdown_docs/__init__.py +0 -0
  23. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/src/pytest_markdown_docs/hooks.py +0 -0
  24. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/src/pytest_markdown_docs/py.typed +0 -0
@@ -0,0 +1,8 @@
1
+ build:
2
+ uv build
3
+
4
+ clean:
5
+ rm -rf dist
6
+
7
+ publish: clean build
8
+ uv publish
@@ -1,3 +1,15 @@
1
+ Metadata-Version: 2.3
2
+ Name: pytest-markdown-docs
3
+ Version: 0.7.1
4
+ Summary: Run markdown code fences through pytest
5
+ Author: Modal Labs
6
+ Author-email: Elias Freider <elias@modal.com>
7
+ License: MIT
8
+ Requires-Python: >=3.8
9
+ Requires-Dist: markdown-it-py<4.0,>=2.2.0
10
+ Requires-Dist: pytest>=7.0.0
11
+ Description-Content-Type: text/markdown
12
+
1
13
  # Pytest Markdown Docs
2
14
 
3
15
  A plugin for [pytest](https://docs.pytest.org) that uses markdown code snippets from markdown files and docstrings as tests.
@@ -136,6 +148,20 @@ assert a + " world" == "hello world"
136
148
  ```
137
149
  ````
138
150
 
151
+ ### Compatibility with Material for MkDocs
152
+
153
+ Material for Mkdocs is not compatible with the default syntax.
154
+
155
+ But if the extension `pymdownx.superfences` is configured for mkdocs, the brace format can be used:
156
+ ````markdown
157
+ ```{.python continuation}
158
+ ````
159
+
160
+ You will need to call pytest with the `--markdown-docs-syntax` option:
161
+ ```shell
162
+ pytest --markdown-docs --markdown-docs-syntax=superfences
163
+ ```
164
+
139
165
  ## MDX Comments for Metadata Options
140
166
  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:
141
167
 
@@ -175,4 +201,4 @@ Or for fun, you can use this plugin to include testing of the validity of snippe
175
201
  * Line numbers are "wrong" for docstring-inlined snippets (since we don't know where in the file the docstring starts)
176
202
  * Line numbers are "wrong" for continuation blocks even in pure markdown files (can be worked out with some refactoring)
177
203
  * 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
204
+ * Assertions are not rewritten w/ pretty data structure inspection like they are with regular pytest tests by default
@@ -1,16 +1,3 @@
1
- Metadata-Version: 2.3
2
- Name: pytest-markdown-docs
3
- Version: 0.6.0
4
- Summary: Run markdown code fences through pytest
5
- Author: Modal Labs
6
- Author-email: Elias Freider <elias@modal.com>
7
- License-Expression: MIT
8
- License-File: LICENSE
9
- Requires-Python: >=3.8
10
- Requires-Dist: markdown-it-py<4.0,>=2.2.0
11
- Requires-Dist: pytest>=7.0.0
12
- Description-Content-Type: text/markdown
13
-
14
1
  # Pytest Markdown Docs
15
2
 
16
3
  A plugin for [pytest](https://docs.pytest.org) that uses markdown code snippets from markdown files and docstrings as tests.
@@ -149,6 +136,20 @@ assert a + " world" == "hello world"
149
136
  ```
150
137
  ````
151
138
 
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
+
152
153
  ## MDX Comments for Metadata Options
153
154
  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:
154
155
 
@@ -188,4 +189,4 @@ Or for fun, you can use this plugin to include testing of the validity of snippe
188
189
  * Line numbers are "wrong" for docstring-inlined snippets (since we don't know where in the file the docstring starts)
189
190
  * Line numbers are "wrong" for continuation blocks even in pure markdown files (can be worked out with some refactoring)
190
191
  * 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".
191
- * 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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pytest-markdown-docs"
3
- version = "0.6.0"
3
+ version = "0.7.1"
4
4
  description = "Run markdown code fences through pytest"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -30,5 +30,3 @@ dev-dependencies = [
30
30
  "pytest~=8.1.0",
31
31
  "ruff>=0.7.0",
32
32
  ]
33
-
34
-
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="WEB_MODULE" version="4">
3
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
4
+ <exclude-output />
5
+ <content url="file://$MODULE_DIR$">
6
+ <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
7
+ <sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
8
+ <excludeFolder url="file://$MODULE_DIR$/dist" />
9
+ </content>
10
+ <orderEntry type="inheritedJdk" />
11
+ <orderEntry type="sourceFolder" forTests="false" />
12
+ </component>
13
+ </module>
@@ -3,12 +3,17 @@ import inspect
3
3
  import traceback
4
4
  import types
5
5
  import pathlib
6
+ from dataclasses import dataclass
7
+
6
8
  import pytest
7
9
  import typing
10
+ from enum import Enum
8
11
 
9
12
  from _pytest._code import ExceptionInfo
10
13
  from _pytest.config.argparsing import Parser
11
14
  from _pytest.pathlib import import_path
15
+ import logging
16
+
12
17
  from pytest_markdown_docs import hooks
13
18
 
14
19
 
@@ -22,9 +27,46 @@ else:
22
27
  if typing.TYPE_CHECKING:
23
28
  from markdown_it.token import Token
24
29
 
30
+ logger = logging.getLogger("pytest-markdown-docs")
31
+
25
32
  MARKER_NAME = "markdown-docs"
26
33
 
27
34
 
35
+ class FenceSyntax(Enum):
36
+ default = "default"
37
+ superfences = "superfences"
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class FenceTest:
42
+ source: str
43
+ fixture_names: typing.Tuple[str, ...]
44
+ start_line: int
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class ObjectTest:
49
+ intra_object_index: int
50
+ object_name: str
51
+ fence_test: FenceTest
52
+
53
+
54
+ def get_docstring_start_line(obj) -> typing.Optional[int]:
55
+ # Get the source lines and the starting line number of the object
56
+ try:
57
+ source_lines, start_line = inspect.getsourcelines(obj)
58
+ except OSError:
59
+ return None
60
+
61
+ # Find the line in the source code that starts with triple quotes (""" or ''')
62
+ for idx, line in enumerate(source_lines):
63
+ line = line.strip()
64
+ if line.startswith(('"""', "'''")):
65
+ return start_line + idx # Return the starting line number
66
+
67
+ return None # Docstring not found in source
68
+
69
+
28
70
  class MarkdownInlinePythonItem(pytest.Item):
29
71
  def __init__(
30
72
  self,
@@ -33,7 +75,6 @@ class MarkdownInlinePythonItem(pytest.Item):
33
75
  code: str,
34
76
  fixture_names: typing.List[str],
35
77
  start_line: int,
36
- fake_line_numbers: bool,
37
78
  ) -> None:
38
79
  super().__init__(name, parent)
39
80
  self.add_marker(MARKER_NAME)
@@ -41,7 +82,6 @@ class MarkdownInlinePythonItem(pytest.Item):
41
82
  self.obj = None
42
83
  self.user_properties.append(("code", code))
43
84
  self.start_line = start_line
44
- self.fake_line_numbers = fake_line_numbers
45
85
  self.fixturenames = fixture_names
46
86
  self.nofuncargs = True
47
87
 
@@ -93,58 +133,47 @@ class MarkdownInlinePythonItem(pytest.Item):
93
133
  excinfo: ExceptionInfo[BaseException],
94
134
  style=None,
95
135
  ):
96
- rawlines = self.code.split("\n")
136
+ rawlines = self.code.rstrip("\n").split("\n")
97
137
 
98
138
  # custom formatted traceback to translate line numbers and markdown files
99
139
  traceback_lines = []
100
140
  stack_summary = traceback.StackSummary.extract(traceback.walk_tb(excinfo.tb))
101
141
  start_capture = False
102
142
 
103
- start_line = 0 if self.fake_line_numbers else self.start_line
143
+ start_line = self.start_line
104
144
 
105
145
  for frame_summary in stack_summary:
106
146
  if frame_summary.filename == str(self.path):
107
- lineno = (frame_summary.lineno or 0) + start_line
108
- start_capture = (
109
- True # start capturing frames the first time we enter user code
110
- )
111
- line = (
112
- rawlines[frame_summary.lineno - 1] if frame_summary.lineno else ""
113
- )
114
- else:
115
- lineno = frame_summary.lineno or 0
116
- line = frame_summary.line or ""
147
+ # start capturing frames the first time we enter user code
148
+ start_capture = True
117
149
 
118
150
  if start_capture:
151
+ lineno = frame_summary.lineno
152
+ line = frame_summary.line or ""
119
153
  linespec = f"line {lineno}"
120
- if self.fake_line_numbers:
121
- linespec = f"code block line {lineno}*"
122
-
123
154
  traceback_lines.append(
124
155
  f""" File "{frame_summary.filename}", {linespec}, in {frame_summary.name}"""
125
156
  )
126
157
  traceback_lines.append(f" {line.lstrip()}")
127
158
 
128
- maxnum = len(str(len(rawlines) + start_line + 1))
159
+ maxdigits = len(str(len(rawlines)))
160
+ code_margin = " "
129
161
  numbered_code = "\n".join(
130
162
  [
131
- f"{i:>{maxnum}} {line}"
132
- for i, line in enumerate(rawlines, start_line + 1)
163
+ f"{i:>{maxdigits}}{code_margin}{line}"
164
+ for i, line in enumerate(rawlines[start_line:], start_line + 1)
133
165
  ]
134
166
  )
135
167
 
136
168
  pretty_traceback = "\n".join(traceback_lines)
137
- note = ""
138
- if self.fake_line_numbers:
139
- note = ", *-denoted line numbers refer to code block"
140
- pt = f"""Traceback (most recent call last{note}):
169
+ pt = f"""Traceback (most recent call last):
141
170
  {pretty_traceback}
142
171
  {excinfo.exconly()}"""
143
172
 
144
173
  return f"""Error in code block:
145
- ```
174
+ {maxdigits * " "}{code_margin}```
146
175
  {numbered_code}
147
- ```
176
+ {maxdigits * " "}{code_margin}```
148
177
  {pt}
149
178
  """
150
179
 
@@ -152,10 +181,12 @@ class MarkdownInlinePythonItem(pytest.Item):
152
181
  return self.name, 0, self.nodeid
153
182
 
154
183
 
155
- def extract_code_blocks(
184
+ def extract_fence_tests(
156
185
  markdown_string: str,
186
+ start_line_offset: int,
157
187
  markdown_type: str = "md",
158
- ) -> typing.Generator[typing.Tuple[str, typing.List[str], int], None, None]:
188
+ fence_syntax: FenceSyntax = FenceSyntax.default,
189
+ ) -> typing.Generator[FenceTest, None, None]:
159
190
  import markdown_it
160
191
 
161
192
  mi = markdown_it.MarkdownIt(config="commonmark")
@@ -166,8 +197,10 @@ def extract_code_blocks(
166
197
  if block.type != "fence" or not block.map:
167
198
  continue
168
199
 
169
- startline = block.map[0] + 1 # skip the info line when counting
170
- code_info = block.info.split()
200
+ if fence_syntax == FenceSyntax.superfences:
201
+ code_info = parse_superfences_block_info(block.info)
202
+ else:
203
+ code_info = block.info.split()
171
204
 
172
205
  lang = code_info[0] if code_info else None
173
206
  code_options = set(code_info) - {lang}
@@ -187,19 +220,50 @@ def extract_code_blocks(
187
220
  code_options |= extract_options_from_mdx_comment(tokens[i - 2].content)
188
221
 
189
222
  if lang in ("py", "python", "python3") and "notest" not in code_options:
190
- code_block = block.content
223
+ start_line = (
224
+ start_line_offset + block.map[0] + 1
225
+ ) # actual code starts on +1 from the "info" line
226
+ if "continuation" not in code_options:
227
+ prev = ""
191
228
 
192
- if "continuation" in code_options:
193
- code_block = prev + code_block
194
- startline = -1 # this disables proper line numbers, TODO: adjust line numbers *per snippet*
229
+ add_blank_lines = start_line - prev.count("\n")
230
+ code_block = prev + ("\n" * add_blank_lines) + block.content
195
231
 
196
- fixture_names = [
232
+ fixture_names = tuple(
197
233
  f[len("fixture:") :] for f in code_options if f.startswith("fixture:")
198
- ]
199
- yield code_block, fixture_names, startline
234
+ )
235
+ yield FenceTest(code_block, fixture_names, start_line)
200
236
  prev = code_block
201
237
 
202
238
 
239
+ def parse_superfences_block_info(block_info: str) -> typing.List[str]:
240
+ """Parse PyMdown Superfences block info syntax.
241
+
242
+ The default `python continuation` format is not compatible with Material for Mkdocs.
243
+ But, PyMdown Superfences has a special brace format to add options to code fence blocks: `{.<lang> <option1> <option2>}`.
244
+
245
+ This function also works if the default syntax is used to allow for mixed usage.
246
+ """
247
+ block_info = block_info.strip()
248
+
249
+ if not block_info.startswith("{"):
250
+ # default syntax
251
+ return block_info.split()
252
+
253
+ block_info = block_info.strip("{}")
254
+ code_info = block_info.split()
255
+ # Lang may not be the first but is always the first element that starts with a dot.
256
+ # (https://facelessuser.github.io/pymdown-extensions/extensions/superfences/#injecting-classes-ids-and-attributes)
257
+ dot_lang = next(
258
+ (info_part for info_part in code_info if info_part.startswith(".")), None
259
+ )
260
+ if dot_lang:
261
+ code_info.remove(dot_lang)
262
+ lang = dot_lang[1:]
263
+ code_info.insert(0, lang)
264
+ return code_info
265
+
266
+
203
267
  def is_mdx_comment(block: "Token") -> bool:
204
268
  return (
205
269
  block.type == "inline"
@@ -219,29 +283,6 @@ def extract_options_from_mdx_comment(comment: str) -> typing.Set[str]:
219
283
  return set(option.strip() for option in comment.split(" ") if option)
220
284
 
221
285
 
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
-
245
286
  class MarkdownDocstringCodeModule(pytest.Module):
246
287
  def collect(self):
247
288
  if pytest.version_tuple >= (8, 1, 0):
@@ -250,45 +291,90 @@ class MarkdownDocstringCodeModule(pytest.Module):
250
291
  self.path, root=self.config.rootpath, consider_namespace_packages=True
251
292
  )
252
293
  else:
253
- # but unsupported before 8.1...
294
+ # but unsupported before pytest 8.1...
254
295
  module = import_path(self.path, root=self.config.rootpath)
255
296
 
256
- for code_block_idx, obj, (
257
- test_code,
258
- fixture_names,
259
- start_line,
260
- ) in find_object_tests_recursive(module.__name__, module):
261
- obj_name = (
262
- getattr(obj, "__qualname__", None)
263
- or getattr(obj, "__name__", None)
264
- or "<Unnamed obj>"
265
- )
297
+ for object_test in self.find_object_tests_recursive(
298
+ module.__name__, module, set(), set()
299
+ ):
300
+ fence_test = object_test.fence_test
266
301
  yield MarkdownInlinePythonItem.from_parent(
267
302
  self,
268
- name=f"{obj_name}[CodeBlock#{code_block_idx+1}][rel.line:{start_line}]",
269
- code=test_code,
270
- fixture_names=fixture_names,
271
- start_line=start_line,
272
- fake_line_numbers=True, # TODO: figure out where docstrings are in file to offset line numbers properly
303
+ name=f"{object_test.object_name}[CodeFence#{object_test.intra_object_index+1}][line:{fence_test.start_line}]",
304
+ code=fence_test.source,
305
+ fixture_names=fence_test.fixture_names,
306
+ start_line=fence_test.start_line,
273
307
  )
274
308
 
309
+ def find_object_tests_recursive(
310
+ self,
311
+ module_name: str,
312
+ object: typing.Any,
313
+ _visited_objects: typing.Set[int],
314
+ _found_tests: typing.Set[typing.Tuple[str, int]],
315
+ ) -> typing.Generator[ObjectTest, None, None]:
316
+ if id(object) in _visited_objects:
317
+ return
318
+ _visited_objects.add(id(object))
319
+ docstr = inspect.getdoc(object)
320
+
321
+ for member_name, member in inspect.getmembers(object):
322
+ if (
323
+ inspect.isclass(member)
324
+ or inspect.isfunction(member)
325
+ or inspect.ismethod(member)
326
+ ) and member.__module__ == module_name:
327
+ yield from self.find_object_tests_recursive(
328
+ module_name, member, _visited_objects, _found_tests
329
+ )
330
+
331
+ if docstr:
332
+ docstring_offset = get_docstring_start_line(object)
333
+ if docstring_offset is None:
334
+ logger.warning(
335
+ f"Could not find line number offset for docstring: {docstr}"
336
+ )
337
+ else:
338
+ obj_name = (
339
+ getattr(object, "__qualname__", None)
340
+ or getattr(object, "__name__", None)
341
+ or "<Unnamed obj>"
342
+ )
343
+ fence_syntax = FenceSyntax(self.config.option.markdowndocs_syntax)
344
+ for i, fence_test in enumerate(
345
+ extract_fence_tests(
346
+ docstr, docstring_offset, fence_syntax=fence_syntax
347
+ )
348
+ ):
349
+ found_test = ObjectTest(i, obj_name, fence_test)
350
+ found_test_location = (
351
+ module_name,
352
+ found_test.fence_test.start_line,
353
+ )
354
+ if found_test_location not in _found_tests:
355
+ _found_tests.add(found_test_location)
356
+ yield found_test
357
+
275
358
 
276
359
  class MarkdownTextFile(pytest.File):
277
360
  def collect(self):
278
361
  markdown_content = self.path.read_text("utf8")
279
-
280
- for i, (code_block, fixture_names, start_line) in enumerate(
281
- extract_code_blocks(
282
- markdown_content, markdown_type=self.path.suffix.replace(".", "")
362
+ fence_syntax = FenceSyntax(self.config.option.markdowndocs_syntax)
363
+
364
+ for i, fence_test in enumerate(
365
+ extract_fence_tests(
366
+ markdown_content,
367
+ start_line_offset=0,
368
+ markdown_type=self.path.suffix.replace(".", ""),
369
+ fence_syntax=fence_syntax,
283
370
  )
284
371
  ):
285
372
  yield MarkdownInlinePythonItem.from_parent(
286
373
  self,
287
- name=f"[CodeBlock#{i+1}][line:{start_line}]",
288
- code=code_block,
289
- fixture_names=fixture_names,
290
- start_line=start_line,
291
- fake_line_numbers=start_line == -1,
374
+ name=f"[CodeFence#{i+1}][line:{fence_test.start_line}]",
375
+ code=fence_test.source,
376
+ fixture_names=fence_test.fixture_names,
377
+ start_line=fence_test.start_line,
292
378
  )
293
379
 
294
380
 
@@ -321,6 +407,14 @@ def pytest_addoption(parser: Parser) -> None:
321
407
  help="run ",
322
408
  dest="markdowndocs",
323
409
  )
410
+ group.addoption(
411
+ "--markdown-docs-syntax",
412
+ action="store",
413
+ choices=[choice.value for choice in FenceSyntax],
414
+ default="default",
415
+ help="Choose an alternative fences syntax",
416
+ dest="markdowndocs_syntax",
417
+ )
324
418
 
325
419
 
326
420
  def pytest_addhooks(pluginmanager):
@@ -0,0 +1,10 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ pytest_plugins = ["pytester"]
6
+
7
+
8
+ @pytest.fixture()
9
+ def support_dir():
10
+ return Path(__file__).parent / "support"
@@ -1,4 +1,7 @@
1
1
  import re
2
+
3
+ from _pytest.pytester import LineMatcher
4
+
2
5
  import pytest_markdown_docs # hack: used for storing a side effect in one of the tests
3
6
 
4
7
 
@@ -122,7 +125,7 @@ def test_traceback(testdir):
122
125
  # we check the traceback vs a regex pattern since the file paths can change
123
126
  expected_output_pattern = r"""
124
127
  Error in code block:
125
- ```
128
+ ```
126
129
  4 def foo\(\):
127
130
  5 raise Exception\("doh"\)
128
131
  6
@@ -130,8 +133,7 @@ Error in code block:
130
133
  8 foo\(\)
131
134
  9
132
135
  10 foo\(\)
133
- 11
134
- ```
136
+ ```
135
137
  Traceback \(most recent call last\):
136
138
  File ".*/test_traceback.md", line 10, in <module>
137
139
  foo\(\)
@@ -350,3 +352,82 @@ def test_notest_mdx_comment(testdir):
350
352
  )
351
353
  result = testdir.runpytest("--markdown-docs")
352
354
  result.assert_outcomes(passed=0)
355
+
356
+
357
+ def test_superfences_format_markdown(testdir):
358
+ testdir.makefile(
359
+ ".md",
360
+ """
361
+ ```python
362
+ b = "hello"
363
+ ```
364
+
365
+ ```{.python continuation}
366
+ assert b + " world" == "hello world"
367
+ ```
368
+
369
+ # the lang may not be the first element
370
+ ```{other_option .python .other-class continuation}
371
+ assert b + " world" == "hello world"
372
+ ```
373
+ """,
374
+ )
375
+ result = testdir.runpytest("--markdown-docs", "--markdown-docs-syntax=superfences")
376
+ result.assert_outcomes(passed=3)
377
+
378
+
379
+ def test_superfences_format_docstring(testdir):
380
+ testdir.makepyfile(
381
+ """
382
+ def simple():
383
+ \"\"\"
384
+ ```python
385
+ b = "hello"
386
+ ```
387
+
388
+ ```{.python continuation}
389
+ assert b + " world" == "hello world"
390
+ ```
391
+ \"\"\"
392
+ """
393
+ )
394
+ result = testdir.runpytest("--markdown-docs", "--markdown-docs-syntax=superfences")
395
+ result.assert_outcomes(passed=2)
396
+
397
+
398
+ def test_error_origin_after_docstring_traceback(testdir, support_dir):
399
+ sample_file = support_dir / "docstring_error_after.py"
400
+ testdir.makepyfile(**{sample_file.stem: sample_file.read_text()})
401
+ result = testdir.runpytest("-v", "--markdown-docs")
402
+
403
+ data: LineMatcher = result.stdout
404
+ data.re_match_lines(
405
+ [
406
+ r"Traceback \(most recent call last\):",
407
+ r'\s*File ".*/docstring_error_after.py", line 5, in <module>',
408
+ r"\s*docstring_error_after.error_after\(\)",
409
+ r'\s*File ".*/docstring_error_after.py", line 11, in error_after',
410
+ r'\s*raise Exception\("bar"\)',
411
+ r"\s*Exception: bar",
412
+ ],
413
+ consecutive=True,
414
+ )
415
+
416
+
417
+ def test_error_origin_before_docstring_traceback(testdir, support_dir):
418
+ sample_file = support_dir / "docstring_error_before.py"
419
+ testdir.makepyfile(**{sample_file.stem: sample_file.read_text()})
420
+ result = testdir.runpytest("-v", "--markdown-docs")
421
+
422
+ data: LineMatcher = result.stdout
423
+ data.re_match_lines(
424
+ [
425
+ r"Traceback \(most recent call last\):",
426
+ r'\s*File ".*/docstring_error_before.py", line 9, in <module>',
427
+ r"\s*docstring_error_before.error_before\(\)",
428
+ r'\s*File ".*/docstring_error_before.py", line 2, in error_before',
429
+ r'\s*raise Exception\("foo"\)',
430
+ r"\s*Exception: foo",
431
+ ],
432
+ consecutive=True,
433
+ )
@@ -0,0 +1,11 @@
1
+ def func():
2
+ """
3
+ ```python
4
+ import docstring_error_after
5
+ docstring_error_after.error_after()
6
+ ```
7
+ """
8
+
9
+
10
+ def error_after():
11
+ raise Exception("bar")
@@ -0,0 +1,11 @@
1
+ def error_before():
2
+ raise Exception("foo")
3
+
4
+
5
+ def func():
6
+ """
7
+ ```python
8
+ import docstring_error_before
9
+ docstring_error_before.error_before()
10
+ ```
11
+ """