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.
- pytest_markdown_docs-0.7.1/Makefile +8 -0
- pytest_markdown_docs-0.6.0/README.md → pytest_markdown_docs-0.7.1/PKG-INFO +27 -1
- pytest_markdown_docs-0.6.0/PKG-INFO → pytest_markdown_docs-0.7.1/README.md +15 -14
- {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/pyproject.toml +1 -3
- pytest_markdown_docs-0.7.1/pytest-markdown-docs.iml +13 -0
- {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/src/pytest_markdown_docs/plugin.py +179 -85
- pytest_markdown_docs-0.7.1/tests/conftest.py +10 -0
- {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/tests/plugin_test.py +84 -3
- pytest_markdown_docs-0.7.1/tests/support/docstring_error_after.py +11 -0
- pytest_markdown_docs-0.7.1/tests/support/docstring_error_before.py +11 -0
- pytest_markdown_docs-0.6.0/poetry.lock +0 -410
- pytest_markdown_docs-0.6.0/poetry.toml +0 -3
- pytest_markdown_docs-0.6.0/tests/conftest.py +0 -1
- pytest_markdown_docs-0.6.0/uv.lock +0 -356
- {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/.github/pull_request_template.md +0 -0
- {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/.github/workflows/check.yml +0 -0
- {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/.github/workflows/ci.yml +0 -0
- {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/.github/workflows/codeql.yml +0 -0
- {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/.gitignore +0 -0
- {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/.pre-commit-config.yaml +0 -0
- {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/LICENSE +0 -0
- {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/src/pytest_markdown_docs/__init__.py +0 -0
- {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/src/pytest_markdown_docs/hooks.py +0 -0
- {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/src/pytest_markdown_docs/py.typed +0 -0
|
@@ -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.
|
|
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>
|
{pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.1}/src/pytest_markdown_docs/plugin.py
RENAMED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
159
|
+
maxdigits = len(str(len(rawlines)))
|
|
160
|
+
code_margin = " "
|
|
129
161
|
numbered_code = "\n".join(
|
|
130
162
|
[
|
|
131
|
-
f"{i:>{
|
|
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
|
-
|
|
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
|
|
184
|
+
def extract_fence_tests(
|
|
156
185
|
markdown_string: str,
|
|
186
|
+
start_line_offset: int,
|
|
157
187
|
markdown_type: str = "md",
|
|
158
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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,
|
|
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
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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"{
|
|
269
|
-
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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"[
|
|
288
|
-
code=
|
|
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):
|
|
@@ -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
|
-
|
|
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
|
+
)
|