pytest-markdown-docs 0.7.0__tar.gz → 0.8.0__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.0 → pytest_markdown_docs-0.8.0}/.github/workflows/ci.yml +1 -1
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/.pre-commit-config.yaml +1 -1
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/PKG-INFO +5 -4
- pytest_markdown_docs-0.8.0/playground/conftest.py +17 -0
- pytest_markdown_docs-0.8.0/playground/foo.md +6 -0
- pytest_markdown_docs-0.8.0/playground/foo.py +3 -0
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/pyproject.toml +3 -3
- pytest_markdown_docs-0.8.0/src/pytest_markdown_docs/_runners.py +133 -0
- pytest_markdown_docs-0.8.0/src/pytest_markdown_docs/definitions.py +19 -0
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/src/pytest_markdown_docs/plugin.py +96 -122
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/tests/plugin_test.py +42 -3
- pytest_markdown_docs-0.8.0/uv.lock +379 -0
- pytest_markdown_docs-0.7.0/uv.lock +0 -356
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/.github/pull_request_template.md +0 -0
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/.github/workflows/check.yml +0 -0
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/.github/workflows/codeql.yml +0 -0
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/.gitignore +0 -0
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/LICENSE +0 -0
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/Makefile +0 -0
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/README.md +0 -0
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/pytest-markdown-docs.iml +0 -0
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/src/pytest_markdown_docs/__init__.py +0 -0
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/src/pytest_markdown_docs/hooks.py +0 -0
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/src/pytest_markdown_docs/py.typed +0 -0
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/tests/conftest.py +0 -0
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/tests/support/docstring_error_after.py +0 -0
- {pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/tests/support/docstring_error_before.py +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-markdown-docs
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Run markdown code fences through pytest
|
|
5
5
|
Author: Modal Labs
|
|
6
6
|
Author-email: Elias Freider <elias@modal.com>
|
|
7
|
-
License: MIT
|
|
8
|
-
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.9
|
|
9
10
|
Requires-Dist: markdown-it-py<4.0,>=2.2.0
|
|
10
11
|
Requires-Dist: pytest>=7.0.0
|
|
11
12
|
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import pytest_asyncio
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest_asyncio.fixture()
|
|
9
|
+
async def async_fix():
|
|
10
|
+
await asyncio.sleep(1)
|
|
11
|
+
yield "ahello"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture()
|
|
15
|
+
def non_async_fix():
|
|
16
|
+
time.sleep(1)
|
|
17
|
+
yield "hello"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pytest-markdown-docs"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "Run markdown code fences through pytest"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -8,7 +8,7 @@ authors = [
|
|
|
8
8
|
{ name = "Elias Freider", email = "elias@modal.com" }
|
|
9
9
|
]
|
|
10
10
|
license = "MIT"
|
|
11
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
12
|
dependencies = [
|
|
13
13
|
"markdown-it-py>=2.2.0,<4.0",
|
|
14
14
|
"pytest>=7.0.0",
|
|
@@ -28,5 +28,5 @@ dev-dependencies = [
|
|
|
28
28
|
"mypy>=1.12.1",
|
|
29
29
|
"pre-commit>=3.5.0",
|
|
30
30
|
"pytest~=8.1.0",
|
|
31
|
-
"ruff
|
|
31
|
+
"ruff~=0.9.10",
|
|
32
32
|
]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import ast
|
|
3
|
+
import traceback
|
|
4
|
+
import typing
|
|
5
|
+
from abc import abstractmethod
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from pytest_markdown_docs.definitions import FenceTestDefinition
|
|
10
|
+
|
|
11
|
+
_default_runner: typing.Optional["_Runner"] = None
|
|
12
|
+
_registered_runners = {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _Runner(metaclass=abc.ABCMeta):
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def runtest(self, test: FenceTestDefinition, args: dict[str, typing.Any]): ...
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def repr_failure(
|
|
21
|
+
self,
|
|
22
|
+
test: FenceTestDefinition,
|
|
23
|
+
excinfo: pytest.ExceptionInfo[BaseException],
|
|
24
|
+
style=None,
|
|
25
|
+
): ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
RUNNER_TYPE = typing.TypeVar("RUNNER_TYPE", bound=type[_Runner])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def register_runner(*, default: bool = False):
|
|
32
|
+
"""Decorator for adding custom runners
|
|
33
|
+
|
|
34
|
+
e.g.
|
|
35
|
+
@register_runner()
|
|
36
|
+
def my_runner(src):
|
|
37
|
+
exec(src)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def decorator(r: RUNNER_TYPE) -> RUNNER_TYPE:
|
|
41
|
+
global _default_runner
|
|
42
|
+
runner = r()
|
|
43
|
+
_registered_runners[r.__name__] = runner
|
|
44
|
+
if default:
|
|
45
|
+
_default_runner = runner
|
|
46
|
+
return r
|
|
47
|
+
|
|
48
|
+
return decorator
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@register_runner(default=True)
|
|
52
|
+
class DefaultRunner(_Runner):
|
|
53
|
+
def runtest(self, test: FenceTestDefinition, args):
|
|
54
|
+
try:
|
|
55
|
+
tree = ast.parse(test.source, filename=test.source_path)
|
|
56
|
+
except SyntaxError:
|
|
57
|
+
raise
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# if we don't compile the code, it seems we get name lookup errors
|
|
61
|
+
# for functions etc. when doing cross-calls across inline functions
|
|
62
|
+
compiled = compile(
|
|
63
|
+
tree, filename=test.source_path, mode="exec", dont_inherit=True
|
|
64
|
+
)
|
|
65
|
+
except SyntaxError:
|
|
66
|
+
raise
|
|
67
|
+
|
|
68
|
+
exec(compiled, args)
|
|
69
|
+
|
|
70
|
+
def repr_failure(
|
|
71
|
+
self,
|
|
72
|
+
test: FenceTestDefinition,
|
|
73
|
+
excinfo: pytest.ExceptionInfo[BaseException],
|
|
74
|
+
style=None,
|
|
75
|
+
):
|
|
76
|
+
"""This renders a traceback starting at the stack from of the code fence
|
|
77
|
+
|
|
78
|
+
Also displays a line-numbered excerpt of the code fence that ran.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
rawlines = test.source.rstrip("\n").split("\n")
|
|
82
|
+
|
|
83
|
+
# custom formatted traceback to translate line numbers and markdown files
|
|
84
|
+
traceback_lines = []
|
|
85
|
+
stack_summary = traceback.StackSummary.extract(traceback.walk_tb(excinfo.tb))
|
|
86
|
+
start_capture = False
|
|
87
|
+
|
|
88
|
+
start_line = test.start_line
|
|
89
|
+
|
|
90
|
+
for frame_summary in stack_summary:
|
|
91
|
+
if frame_summary.filename == str(test.source_path):
|
|
92
|
+
# start capturing frames the first time we enter user code
|
|
93
|
+
start_capture = True
|
|
94
|
+
|
|
95
|
+
if start_capture:
|
|
96
|
+
lineno = frame_summary.lineno
|
|
97
|
+
line = frame_summary.line or ""
|
|
98
|
+
linespec = f"line {lineno}"
|
|
99
|
+
traceback_lines.append(
|
|
100
|
+
f""" File "{frame_summary.filename}", {linespec}, in {frame_summary.name}"""
|
|
101
|
+
)
|
|
102
|
+
traceback_lines.append(f" {line.lstrip()}")
|
|
103
|
+
|
|
104
|
+
maxdigits = len(str(len(rawlines)))
|
|
105
|
+
code_margin = " "
|
|
106
|
+
numbered_code = "\n".join(
|
|
107
|
+
[
|
|
108
|
+
f"{i:>{maxdigits}}{code_margin}{line}"
|
|
109
|
+
for i, line in enumerate(rawlines[start_line:], start_line + 1)
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
pretty_traceback = "\n".join(traceback_lines)
|
|
114
|
+
pt = f"""Traceback (most recent call last):
|
|
115
|
+
{pretty_traceback}
|
|
116
|
+
{excinfo.exconly()}"""
|
|
117
|
+
|
|
118
|
+
return f"""Error in code block:
|
|
119
|
+
{maxdigits * " "}{code_margin}```
|
|
120
|
+
{numbered_code}
|
|
121
|
+
{maxdigits * " "}{code_margin}```
|
|
122
|
+
{pt}
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_runner(name: typing.Optional[str]) -> _Runner:
|
|
127
|
+
if name is None:
|
|
128
|
+
assert _default_runner is not None
|
|
129
|
+
return _default_runner
|
|
130
|
+
|
|
131
|
+
if name not in _registered_runners:
|
|
132
|
+
raise Exception(f"No such pytest-markdown-docs runner: {name}")
|
|
133
|
+
return _registered_runners[name]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import typing
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class FenceTestDefinition:
|
|
8
|
+
source: str
|
|
9
|
+
fixture_names: typing.Sequence[str]
|
|
10
|
+
start_line: int
|
|
11
|
+
source_path: pathlib.Path
|
|
12
|
+
runner_name: typing.Optional[str]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class ObjectTestDefinition:
|
|
17
|
+
intra_object_index: int
|
|
18
|
+
object_name: str
|
|
19
|
+
fence_test: FenceTestDefinition
|
{pytest_markdown_docs-0.7.0 → pytest_markdown_docs-0.8.0}/src/pytest_markdown_docs/plugin.py
RENAMED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import ast
|
|
2
1
|
import inspect
|
|
3
|
-
import traceback
|
|
4
2
|
import types
|
|
5
3
|
import pathlib
|
|
6
|
-
from dataclasses import dataclass
|
|
7
4
|
|
|
8
5
|
import pytest
|
|
9
6
|
import typing
|
|
@@ -15,7 +12,8 @@ from _pytest.pathlib import import_path
|
|
|
15
12
|
import logging
|
|
16
13
|
|
|
17
14
|
from pytest_markdown_docs import hooks
|
|
18
|
-
|
|
15
|
+
from pytest_markdown_docs.definitions import FenceTestDefinition, ObjectTestDefinition
|
|
16
|
+
from pytest_markdown_docs._runners import get_runner
|
|
19
17
|
|
|
20
18
|
if pytest.version_tuple >= (8, 0, 0):
|
|
21
19
|
from _pytest.fixtures import TopRequest
|
|
@@ -37,23 +35,12 @@ class FenceSyntax(Enum):
|
|
|
37
35
|
superfences = "superfences"
|
|
38
36
|
|
|
39
37
|
|
|
40
|
-
@dataclass
|
|
41
|
-
class FenceTest:
|
|
42
|
-
source: str
|
|
43
|
-
fixture_names: typing.List[str]
|
|
44
|
-
start_line: int
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@dataclass
|
|
48
|
-
class ObjectTest:
|
|
49
|
-
intra_object_index: int
|
|
50
|
-
object_name: str
|
|
51
|
-
fence_test: FenceTest
|
|
52
|
-
|
|
53
|
-
|
|
54
38
|
def get_docstring_start_line(obj) -> typing.Optional[int]:
|
|
55
39
|
# Get the source lines and the starting line number of the object
|
|
56
|
-
|
|
40
|
+
try:
|
|
41
|
+
source_lines, start_line = inspect.getsourcelines(obj)
|
|
42
|
+
except OSError:
|
|
43
|
+
return None
|
|
57
44
|
|
|
58
45
|
# Find the line in the source code that starts with triple quotes (""" or ''')
|
|
59
46
|
for idx, line in enumerate(source_lines):
|
|
@@ -69,18 +56,18 @@ class MarkdownInlinePythonItem(pytest.Item):
|
|
|
69
56
|
self,
|
|
70
57
|
name: str,
|
|
71
58
|
parent: typing.Union["MarkdownDocstringCodeModule", "MarkdownTextFile"],
|
|
72
|
-
|
|
73
|
-
fixture_names: typing.List[str],
|
|
74
|
-
start_line: int,
|
|
59
|
+
test_definition: FenceTestDefinition,
|
|
75
60
|
) -> None:
|
|
76
61
|
super().__init__(name, parent)
|
|
77
62
|
self.add_marker(MARKER_NAME)
|
|
78
|
-
self.code =
|
|
63
|
+
self.code = test_definition.source
|
|
79
64
|
self.obj = None
|
|
80
|
-
self.
|
|
81
|
-
self.
|
|
82
|
-
self.
|
|
65
|
+
self.test_definition = test_definition
|
|
66
|
+
self.user_properties.append(("code", test_definition.source))
|
|
67
|
+
self.start_line = test_definition.start_line
|
|
68
|
+
self.fixturenames = test_definition.fixture_names
|
|
83
69
|
self.nofuncargs = True
|
|
70
|
+
self.runner_name = test_definition.runner_name
|
|
84
71
|
|
|
85
72
|
def setup(self):
|
|
86
73
|
def func() -> None:
|
|
@@ -92,6 +79,7 @@ class MarkdownInlinePythonItem(pytest.Item):
|
|
|
92
79
|
)
|
|
93
80
|
self.fixture_request = TopRequest(self, _ispytest=True)
|
|
94
81
|
self.fixture_request._fillfixtures()
|
|
82
|
+
self.runner = get_runner(self.runner_name)
|
|
95
83
|
|
|
96
84
|
def runtest(self):
|
|
97
85
|
global_sets = self.parent.config.hook.pytest_markdown_docs_globals()
|
|
@@ -111,79 +99,36 @@ class MarkdownInlinePythonItem(pytest.Item):
|
|
|
111
99
|
for argname, value in self.funcargs.items():
|
|
112
100
|
all_globals[argname] = value
|
|
113
101
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
try:
|
|
120
|
-
# if we don't compile the code, it seems we get name lookup errors
|
|
121
|
-
# for functions etc. when doing cross-calls across inline functions
|
|
122
|
-
compiled = compile(tree, filename=self.path, mode="exec", dont_inherit=True)
|
|
123
|
-
except SyntaxError:
|
|
124
|
-
raise
|
|
125
|
-
|
|
126
|
-
exec(compiled, all_globals)
|
|
102
|
+
# this ensures that pytest's stdout/stderr capture works during the test:
|
|
103
|
+
capman = self.config.pluginmanager.getplugin("capturemanager")
|
|
104
|
+
with capman.global_and_fixture_disabled():
|
|
105
|
+
self.runner.runtest(self.test_definition, all_globals)
|
|
127
106
|
|
|
128
107
|
def repr_failure(
|
|
129
108
|
self,
|
|
130
109
|
excinfo: ExceptionInfo[BaseException],
|
|
131
110
|
style=None,
|
|
132
|
-
):
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
# custom formatted traceback to translate line numbers and markdown files
|
|
136
|
-
traceback_lines = []
|
|
137
|
-
stack_summary = traceback.StackSummary.extract(traceback.walk_tb(excinfo.tb))
|
|
138
|
-
start_capture = False
|
|
139
|
-
|
|
140
|
-
start_line = self.start_line
|
|
141
|
-
|
|
142
|
-
for frame_summary in stack_summary:
|
|
143
|
-
if frame_summary.filename == str(self.path):
|
|
144
|
-
# start capturing frames the first time we enter user code
|
|
145
|
-
start_capture = True
|
|
146
|
-
|
|
147
|
-
if start_capture:
|
|
148
|
-
lineno = frame_summary.lineno
|
|
149
|
-
line = frame_summary.line or ""
|
|
150
|
-
linespec = f"line {lineno}"
|
|
151
|
-
traceback_lines.append(
|
|
152
|
-
f""" File "{frame_summary.filename}", {linespec}, in {frame_summary.name}"""
|
|
153
|
-
)
|
|
154
|
-
traceback_lines.append(f" {line.lstrip()}")
|
|
155
|
-
|
|
156
|
-
maxdigits = len(str(len(rawlines)))
|
|
157
|
-
code_margin = " "
|
|
158
|
-
numbered_code = "\n".join(
|
|
159
|
-
[
|
|
160
|
-
f"{i:>{maxdigits}}{code_margin}{line}"
|
|
161
|
-
for i, line in enumerate(rawlines[start_line:], start_line + 1)
|
|
162
|
-
]
|
|
163
|
-
)
|
|
111
|
+
) -> str:
|
|
112
|
+
return self.runner.repr_failure(self.test_definition, excinfo, style)
|
|
164
113
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
{pretty_traceback}
|
|
168
|
-
{excinfo.exconly()}"""
|
|
114
|
+
def reportinfo(self):
|
|
115
|
+
return self.path, self.start_line, self.name
|
|
169
116
|
|
|
170
|
-
return f"""Error in code block:
|
|
171
|
-
{maxdigits * " "}{code_margin}```
|
|
172
|
-
{numbered_code}
|
|
173
|
-
{maxdigits * " "}{code_margin}```
|
|
174
|
-
{pt}
|
|
175
|
-
"""
|
|
176
117
|
|
|
177
|
-
|
|
178
|
-
|
|
118
|
+
def get_prefixed_strings(
|
|
119
|
+
seq: typing.Collection[str], prefix: str
|
|
120
|
+
) -> typing.Sequence[str]:
|
|
121
|
+
# return strings matching a prefix, with the prefix stripped
|
|
122
|
+
return tuple(s[len(prefix) :] for s in seq if s.startswith(prefix))
|
|
179
123
|
|
|
180
124
|
|
|
181
125
|
def extract_fence_tests(
|
|
182
126
|
markdown_string: str,
|
|
183
127
|
start_line_offset: int,
|
|
128
|
+
source_path: pathlib.Path,
|
|
184
129
|
markdown_type: str = "md",
|
|
185
130
|
fence_syntax: FenceSyntax = FenceSyntax.default,
|
|
186
|
-
) -> typing.Generator[
|
|
131
|
+
) -> typing.Generator[FenceTestDefinition, None, None]:
|
|
187
132
|
import markdown_it
|
|
188
133
|
|
|
189
134
|
mi = markdown_it.MarkdownIt(config="commonmark")
|
|
@@ -226,10 +171,23 @@ def extract_fence_tests(
|
|
|
226
171
|
add_blank_lines = start_line - prev.count("\n")
|
|
227
172
|
code_block = prev + ("\n" * add_blank_lines) + block.content
|
|
228
173
|
|
|
229
|
-
fixture_names =
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
174
|
+
fixture_names = get_prefixed_strings(code_options, "fixture:")
|
|
175
|
+
runner_names = get_prefixed_strings(code_options, "runner:")
|
|
176
|
+
if len(runner_names) == 0:
|
|
177
|
+
runner_name = None
|
|
178
|
+
elif len(runner_names) > 1:
|
|
179
|
+
raise Exception(
|
|
180
|
+
f"Multiple runners are not supported, use a single one instead: {runner_names}"
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
runner_name = runner_names[0]
|
|
184
|
+
yield FenceTestDefinition(
|
|
185
|
+
code_block,
|
|
186
|
+
fixture_names,
|
|
187
|
+
start_line,
|
|
188
|
+
source_path=source_path,
|
|
189
|
+
runner_name=runner_name,
|
|
190
|
+
)
|
|
233
191
|
prev = code_block
|
|
234
192
|
|
|
235
193
|
|
|
@@ -291,50 +249,67 @@ class MarkdownDocstringCodeModule(pytest.Module):
|
|
|
291
249
|
# but unsupported before pytest 8.1...
|
|
292
250
|
module = import_path(self.path, root=self.config.rootpath)
|
|
293
251
|
|
|
294
|
-
for object_test in self.find_object_tests_recursive(
|
|
252
|
+
for object_test in self.find_object_tests_recursive(
|
|
253
|
+
module.__name__, module, set(), set()
|
|
254
|
+
):
|
|
295
255
|
fence_test = object_test.fence_test
|
|
296
256
|
yield MarkdownInlinePythonItem.from_parent(
|
|
297
257
|
self,
|
|
298
|
-
name=f"{object_test.object_name}[CodeFence#{object_test.intra_object_index+1}][line:{fence_test.start_line}]",
|
|
299
|
-
|
|
300
|
-
fixture_names=fence_test.fixture_names,
|
|
301
|
-
start_line=fence_test.start_line,
|
|
258
|
+
name=f"{object_test.object_name}[CodeFence#{object_test.intra_object_index + 1}][line:{fence_test.start_line}]",
|
|
259
|
+
test_definition=fence_test,
|
|
302
260
|
)
|
|
303
261
|
|
|
304
262
|
def find_object_tests_recursive(
|
|
305
|
-
self,
|
|
306
|
-
|
|
263
|
+
self,
|
|
264
|
+
module_name: str,
|
|
265
|
+
object: typing.Any,
|
|
266
|
+
_visited_objects: typing.Set[int],
|
|
267
|
+
_found_tests: typing.Set[typing.Tuple[str, int]],
|
|
268
|
+
) -> typing.Generator[ObjectTestDefinition, None, None]:
|
|
269
|
+
if id(object) in _visited_objects:
|
|
270
|
+
return
|
|
271
|
+
_visited_objects.add(id(object))
|
|
307
272
|
docstr = inspect.getdoc(object)
|
|
308
273
|
|
|
309
|
-
if docstr:
|
|
310
|
-
docstring_offset = get_docstring_start_line(object)
|
|
311
|
-
if docstring_offset is None:
|
|
312
|
-
logger.warning(
|
|
313
|
-
"Could not find line number offset for docstring: {docstr}"
|
|
314
|
-
)
|
|
315
|
-
docstring_offset = 0
|
|
316
|
-
|
|
317
|
-
obj_name = (
|
|
318
|
-
getattr(object, "__qualname__", None)
|
|
319
|
-
or getattr(object, "__name__", None)
|
|
320
|
-
or "<Unnamed obj>"
|
|
321
|
-
)
|
|
322
|
-
fence_syntax = FenceSyntax(self.config.option.markdowndocs_syntax)
|
|
323
|
-
for i, fence_test in enumerate(
|
|
324
|
-
extract_fence_tests(docstr, docstring_offset, fence_syntax=fence_syntax)
|
|
325
|
-
):
|
|
326
|
-
yield ObjectTest(i, obj_name, fence_test)
|
|
327
|
-
|
|
328
274
|
for member_name, member in inspect.getmembers(object):
|
|
329
|
-
if member_name.startswith("_"):
|
|
330
|
-
continue
|
|
331
|
-
|
|
332
275
|
if (
|
|
333
276
|
inspect.isclass(member)
|
|
334
277
|
or inspect.isfunction(member)
|
|
335
278
|
or inspect.ismethod(member)
|
|
336
279
|
) and member.__module__ == module_name:
|
|
337
|
-
yield from self.find_object_tests_recursive(
|
|
280
|
+
yield from self.find_object_tests_recursive(
|
|
281
|
+
module_name, member, _visited_objects, _found_tests
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
if docstr:
|
|
285
|
+
docstring_offset = get_docstring_start_line(object)
|
|
286
|
+
if docstring_offset is None:
|
|
287
|
+
logger.warning(
|
|
288
|
+
f"Could not find line number offset for docstring: {docstr}"
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
obj_name = (
|
|
292
|
+
getattr(object, "__qualname__", None)
|
|
293
|
+
or getattr(object, "__name__", None)
|
|
294
|
+
or "<Unnamed obj>"
|
|
295
|
+
)
|
|
296
|
+
fence_syntax = FenceSyntax(self.config.option.markdowndocs_syntax)
|
|
297
|
+
for i, fence_test in enumerate(
|
|
298
|
+
extract_fence_tests(
|
|
299
|
+
docstr,
|
|
300
|
+
docstring_offset,
|
|
301
|
+
source_path=self.path,
|
|
302
|
+
fence_syntax=fence_syntax,
|
|
303
|
+
)
|
|
304
|
+
):
|
|
305
|
+
found_test = ObjectTestDefinition(i, obj_name, fence_test)
|
|
306
|
+
found_test_location = (
|
|
307
|
+
module_name,
|
|
308
|
+
found_test.fence_test.start_line,
|
|
309
|
+
)
|
|
310
|
+
if found_test_location not in _found_tests:
|
|
311
|
+
_found_tests.add(found_test_location)
|
|
312
|
+
yield found_test
|
|
338
313
|
|
|
339
314
|
|
|
340
315
|
class MarkdownTextFile(pytest.File):
|
|
@@ -345,6 +320,7 @@ class MarkdownTextFile(pytest.File):
|
|
|
345
320
|
for i, fence_test in enumerate(
|
|
346
321
|
extract_fence_tests(
|
|
347
322
|
markdown_content,
|
|
323
|
+
source_path=self.path,
|
|
348
324
|
start_line_offset=0,
|
|
349
325
|
markdown_type=self.path.suffix.replace(".", ""),
|
|
350
326
|
fence_syntax=fence_syntax,
|
|
@@ -352,10 +328,8 @@ class MarkdownTextFile(pytest.File):
|
|
|
352
328
|
):
|
|
353
329
|
yield MarkdownInlinePythonItem.from_parent(
|
|
354
330
|
self,
|
|
355
|
-
name=f"[CodeFence#{i+1}][line:{fence_test.start_line}]",
|
|
356
|
-
|
|
357
|
-
fixture_names=fence_test.fixture_names,
|
|
358
|
-
start_line=fence_test.start_line,
|
|
331
|
+
name=f"[CodeFence#{i + 1}][line:{fence_test.start_line}]",
|
|
332
|
+
test_definition=fence_test,
|
|
359
333
|
)
|
|
360
334
|
|
|
361
335
|
|
|
@@ -142,9 +142,9 @@ Traceback \(most recent call last\):
|
|
|
142
142
|
Exception: doh
|
|
143
143
|
""".strip()
|
|
144
144
|
pytest_output = "\n".join(line.rstrip() for line in result.outlines).strip()
|
|
145
|
-
assert (
|
|
146
|
-
|
|
147
|
-
)
|
|
145
|
+
assert re.search(expected_output_pattern, pytest_output) is not None, (
|
|
146
|
+
"Output traceback doesn't match expected value"
|
|
147
|
+
)
|
|
148
148
|
|
|
149
149
|
|
|
150
150
|
def test_autouse_fixtures(testdir):
|
|
@@ -431,3 +431,42 @@ def test_error_origin_before_docstring_traceback(testdir, support_dir):
|
|
|
431
431
|
],
|
|
432
432
|
consecutive=True,
|
|
433
433
|
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def test_custom_runner(testdir):
|
|
437
|
+
testdir.makeconftest(
|
|
438
|
+
"""
|
|
439
|
+
import pytest_markdown_docs._runners
|
|
440
|
+
|
|
441
|
+
@pytest_markdown_docs._runners.register_runner()
|
|
442
|
+
class LinesAreAllFoo(pytest_markdown_docs._runners.DefaultRunner):
|
|
443
|
+
def runtest(self, test, args):
|
|
444
|
+
lines = test.source.strip().split("\\n")
|
|
445
|
+
for line in lines:
|
|
446
|
+
assert line == "foo"
|
|
447
|
+
"""
|
|
448
|
+
)
|
|
449
|
+
testdir.makefile(
|
|
450
|
+
".md",
|
|
451
|
+
"""
|
|
452
|
+
```python runner:LinesAreAllFoo
|
|
453
|
+
foo
|
|
454
|
+
foo
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
```python runner:LinesAreAllFoo
|
|
458
|
+
foo
|
|
459
|
+
bar
|
|
460
|
+
```
|
|
461
|
+
""",
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
result = testdir.runpytest("-v", "--markdown-docs")
|
|
465
|
+
result.assert_outcomes(passed=1, failed=1)
|
|
466
|
+
result.stdout.re_match_lines(
|
|
467
|
+
[
|
|
468
|
+
r".*\[CodeFence#1\]\[line:1\].*PASSED.*",
|
|
469
|
+
r".*\[CodeFence#2\]\[line:6\].*FAILED.*",
|
|
470
|
+
],
|
|
471
|
+
consecutive=True,
|
|
472
|
+
)
|