pytest-codeblock 0.5.6__tar.gz → 0.5.8__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_codeblock-0.5.6/src/pytest_codeblock.egg-info → pytest_codeblock-0.5.8}/PKG-INFO +8 -1
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/README.rst +6 -0
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/pyproject.toml +30 -2
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock/__init__.py +1 -1
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock/collector.py +7 -7
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock/md.py +23 -12
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock/rst.py +17 -2
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock/tests/test_customisation.py +2 -2
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock/tests/test_integration.py +66 -66
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock/tests/test_nameless_codeblocks.py +38 -38
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8/src/pytest_codeblock.egg-info}/PKG-INFO +8 -1
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock.egg-info/requires.txt +1 -0
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/LICENSE +0 -0
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/setup.cfg +0 -0
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock/config.py +0 -0
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock/constants.py +0 -0
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock/helpers.py +0 -0
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock/pytestrun.py +0 -0
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock/tests/__init__.py +0 -0
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock/tests/test_pytest_codeblock.py +0 -0
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock/tests/test_pytestrun_marker.py +0 -0
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock.egg-info/SOURCES.txt +0 -0
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock.egg-info/dependency_links.txt +0 -0
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock.egg-info/entry_points.txt +0 -0
- {pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-codeblock
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.8
|
|
4
4
|
Summary: Pytest plugin to collect and test code blocks in reStructuredText and Markdown files.
|
|
5
5
|
Author-email: Artur Barseghyan <artur.barseghyan@gmail.com>
|
|
6
6
|
Maintainer-email: Artur Barseghyan <artur.barseghyan@gmail.com>
|
|
@@ -48,6 +48,7 @@ Requires-Dist: pytest; extra == "test"
|
|
|
48
48
|
Requires-Dist: pytest-cov; extra == "test"
|
|
49
49
|
Requires-Dist: pytest-django; extra == "test"
|
|
50
50
|
Requires-Dist: respx; extra == "test"
|
|
51
|
+
Requires-Dist: langchain-tests; extra == "test"
|
|
51
52
|
Provides-Extra: docs
|
|
52
53
|
Requires-Dist: sphinx; extra == "docs"
|
|
53
54
|
Requires-Dist: sphinx-autobuild; extra == "docs"
|
|
@@ -78,6 +79,7 @@ pytest-codeblock
|
|
|
78
79
|
.. _openai: https://github.com/openai/openai-python
|
|
79
80
|
.. _Ollama: https://github.com/ollama/ollama
|
|
80
81
|
.. _tomli: https://pypi.org/project/tomli/
|
|
82
|
+
.. _doc8: https://doc8.readthedocs.io/
|
|
81
83
|
|
|
82
84
|
.. Internal references
|
|
83
85
|
|
|
@@ -219,6 +221,11 @@ See `customisation docs`_ for more.
|
|
|
219
221
|
|
|
220
222
|
Usage
|
|
221
223
|
=====
|
|
224
|
+
.. note::
|
|
225
|
+
|
|
226
|
+
It's highly recommended to use `doc8`_ for catching possible markup errors,
|
|
227
|
+
that otherwise would be difficult to spot.
|
|
228
|
+
|
|
222
229
|
reStructruredText usage
|
|
223
230
|
-----------------------
|
|
224
231
|
Any code directive, such as ``.. code-block:: python``, ``.. code:: python``,
|
|
@@ -15,6 +15,7 @@ pytest-codeblock
|
|
|
15
15
|
.. _openai: https://github.com/openai/openai-python
|
|
16
16
|
.. _Ollama: https://github.com/ollama/ollama
|
|
17
17
|
.. _tomli: https://pypi.org/project/tomli/
|
|
18
|
+
.. _doc8: https://doc8.readthedocs.io/
|
|
18
19
|
|
|
19
20
|
.. Internal references
|
|
20
21
|
|
|
@@ -156,6 +157,11 @@ See `customisation docs`_ for more.
|
|
|
156
157
|
|
|
157
158
|
Usage
|
|
158
159
|
=====
|
|
160
|
+
.. note::
|
|
161
|
+
|
|
162
|
+
It's highly recommended to use `doc8`_ for catching possible markup errors,
|
|
163
|
+
that otherwise would be difficult to spot.
|
|
164
|
+
|
|
159
165
|
reStructruredText usage
|
|
160
166
|
-----------------------
|
|
161
167
|
Any code directive, such as ``.. code-block:: python``, ``.. code:: python``,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name = "pytest-codeblock"
|
|
3
3
|
description = "Pytest plugin to collect and test code blocks in reStructuredText and Markdown files."
|
|
4
4
|
readme = "README.rst"
|
|
5
|
-
version = "0.5.
|
|
5
|
+
version = "0.5.8"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"pytest",
|
|
@@ -66,6 +66,7 @@ test = [
|
|
|
66
66
|
"pytest-cov",
|
|
67
67
|
"pytest-django",
|
|
68
68
|
"respx",
|
|
69
|
+
"langchain-tests", # Module-scoped fixtures for testing scope resolution
|
|
69
70
|
]
|
|
70
71
|
docs = [
|
|
71
72
|
"sphinx",
|
|
@@ -253,6 +254,7 @@ ignore = [
|
|
|
253
254
|
".idea",
|
|
254
255
|
".vscode",
|
|
255
256
|
".DS_Store",
|
|
257
|
+
".claude",
|
|
256
258
|
"Thumbs.db",
|
|
257
259
|
".ruff_cache",
|
|
258
260
|
".coverage.*",
|
|
@@ -264,13 +266,31 @@ ignore = [
|
|
|
264
266
|
"CODE_OF_CONDUCT.rst",
|
|
265
267
|
"LICENSE",
|
|
266
268
|
"SECURITY.rst",
|
|
269
|
+
"docs/_implement_pytest_hooks.rst",
|
|
267
270
|
"docs/changelog.rst",
|
|
268
271
|
"docs/code_of_conduct.rst",
|
|
272
|
+
"docs/contributor_guidelines.rst",
|
|
273
|
+
"docs/documentation.rst",
|
|
274
|
+
"docs/full-llms.rst",
|
|
275
|
+
"docs/index.rst",
|
|
276
|
+
"docs/llms.rst",
|
|
277
|
+
"docs/package.rst",
|
|
269
278
|
"docs/security.rst",
|
|
270
279
|
"docs/source_tree.rst",
|
|
271
280
|
"docs/source_tree_full.rst",
|
|
272
281
|
"docs/make.bat",
|
|
273
282
|
"docs/Makefile",
|
|
283
|
+
"src/pytest_codeblock/tests/pytest_codeblock_*.py",
|
|
284
|
+
]
|
|
285
|
+
order = [
|
|
286
|
+
"README.rst",
|
|
287
|
+
"CONTRIBUTING.rst",
|
|
288
|
+
"docs/quick_start_ref.rst",
|
|
289
|
+
"docs/restructured_text.rst",
|
|
290
|
+
"docs/markdown.rst",
|
|
291
|
+
"docs/cheatsheet_restructured_text.rst",
|
|
292
|
+
"docs/cheatsheet_markdown.rst",
|
|
293
|
+
"docs/customisation.rst",
|
|
274
294
|
]
|
|
275
295
|
|
|
276
296
|
[[tool.sphinx-source-tree.files]]
|
|
@@ -304,6 +324,7 @@ ignore = [
|
|
|
304
324
|
".idea",
|
|
305
325
|
".vscode",
|
|
306
326
|
".DS_Store",
|
|
327
|
+
".claude",
|
|
307
328
|
"Thumbs.db",
|
|
308
329
|
".ruff_cache",
|
|
309
330
|
".coverage.*",
|
|
@@ -315,13 +336,20 @@ ignore = [
|
|
|
315
336
|
"CODE_OF_CONDUCT.rst",
|
|
316
337
|
"LICENSE",
|
|
317
338
|
"SECURITY.rst",
|
|
339
|
+
"docs/_implement_pytest_hooks.rst",
|
|
318
340
|
"docs/changelog.rst",
|
|
319
341
|
"docs/code_of_conduct.rst",
|
|
342
|
+
"docs/contributor_guidelines.rst",
|
|
343
|
+
"docs/documentation.rst",
|
|
344
|
+
"docs/full-llms.rst",
|
|
345
|
+
"docs/index.rst",
|
|
346
|
+
"docs/llms.rst",
|
|
347
|
+
"docs/package.rst",
|
|
320
348
|
"docs/security.rst",
|
|
321
349
|
"docs/source_tree.rst",
|
|
322
350
|
"docs/source_tree_full.rst",
|
|
323
351
|
"docs/make.bat",
|
|
324
352
|
"docs/Makefile",
|
|
325
353
|
"examples",
|
|
326
|
-
"
|
|
354
|
+
"src/pytest_codeblock/tests/pytest_codeblock_*.py",
|
|
327
355
|
]
|
|
@@ -6,7 +6,7 @@ from .md import MarkdownFile
|
|
|
6
6
|
from .rst import RSTFile
|
|
7
7
|
|
|
8
8
|
__title__ = "pytest-codeblock"
|
|
9
|
-
__version__ = "0.5.
|
|
9
|
+
__version__ = "0.5.8"
|
|
10
10
|
__author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
|
|
11
11
|
__copyright__ = "2025-2026 Artur Barseghyan"
|
|
12
12
|
__license__ = "MIT"
|
|
@@ -28,17 +28,17 @@ def group_snippets(snippets: list[CodeSnippet]) -> list[CodeSnippet]:
|
|
|
28
28
|
"""
|
|
29
29
|
Combine snippets that share a group key, using one of two modes:
|
|
30
30
|
|
|
31
|
-
-
|
|
31
|
+
- Merge mode (default): snippets sharing the same name (no ``group``
|
|
32
32
|
set, or nameless/same-name continuations) are concatenated into a single
|
|
33
|
-
test, accumulating marks and fixtures. This is the
|
|
34
|
-
-
|
|
33
|
+
test, accumulating marks and fixtures. This is the default behaviour.
|
|
34
|
+
- Incremental mode: when every continuation snippet (``group`` set) in
|
|
35
35
|
a group also carries its own distinct name, emit one test per snippet.
|
|
36
36
|
Each test's code is the cumulative concatenation of all preceding
|
|
37
37
|
snippets plus itself, so each step is exercised in isolation.
|
|
38
38
|
|
|
39
39
|
Unnamed snippets receive unique auto-keys so they are never merged.
|
|
40
40
|
"""
|
|
41
|
-
# Pass 1: bucket each snippet by its group key, preserving insertion order
|
|
41
|
+
# Pass 1: bucket each snippet by its group key, preserving insertion order
|
|
42
42
|
buckets: dict[str, list[CodeSnippet]] = {}
|
|
43
43
|
order: list[str] = []
|
|
44
44
|
anon_count = 0
|
|
@@ -57,13 +57,13 @@ def group_snippets(snippets: list[CodeSnippet]) -> list[CodeSnippet]:
|
|
|
57
57
|
order.append(key)
|
|
58
58
|
buckets[key].append(sn)
|
|
59
59
|
|
|
60
|
-
# Pass 2: emit merged or incremental snippets per bucket
|
|
60
|
+
# Pass 2: emit merged or incremental snippets per bucket
|
|
61
61
|
combined: list[CodeSnippet] = []
|
|
62
62
|
|
|
63
63
|
for key in order:
|
|
64
64
|
members = buckets[key]
|
|
65
65
|
continuations = [sn for sn in members if sn.group]
|
|
66
|
-
# Incremental only when every continuation has a distinct own name
|
|
66
|
+
# Incremental only when every continuation has a distinct own name
|
|
67
67
|
incremental = continuations and all(
|
|
68
68
|
sn.name and sn.name != key for sn in continuations
|
|
69
69
|
)
|
|
@@ -84,7 +84,7 @@ def group_snippets(snippets: list[CodeSnippet]) -> list[CodeSnippet]:
|
|
|
84
84
|
fixtures=list(acc_fixtures),
|
|
85
85
|
))
|
|
86
86
|
else:
|
|
87
|
-
# Merge mode (
|
|
87
|
+
# Merge mode (default behaviour)
|
|
88
88
|
first = members[0]
|
|
89
89
|
merged_marks = list(first.marks)
|
|
90
90
|
merged_fixtures = list(first.fixtures)
|
|
@@ -3,6 +3,8 @@ import inspect
|
|
|
3
3
|
import re
|
|
4
4
|
import textwrap
|
|
5
5
|
import traceback
|
|
6
|
+
import types
|
|
7
|
+
from collections.abc import Generator
|
|
6
8
|
from typing import Optional
|
|
7
9
|
|
|
8
10
|
import pytest
|
|
@@ -103,7 +105,7 @@ def parse_markdown(text: str) -> list[CodeSnippet]:
|
|
|
103
105
|
block_indent = indent
|
|
104
106
|
start_line = idx + 1
|
|
105
107
|
code_buffer = []
|
|
106
|
-
#
|
|
108
|
+
# Determine name from info string or pending comment
|
|
107
109
|
snippet_name = None
|
|
108
110
|
for token in extra.split():
|
|
109
111
|
if (
|
|
@@ -118,18 +120,17 @@ def parse_markdown(text: str) -> list[CodeSnippet]:
|
|
|
118
120
|
break
|
|
119
121
|
if snippet_name is None:
|
|
120
122
|
snippet_name = pending_name
|
|
121
|
-
#
|
|
123
|
+
# Reset pending_name; marks stay until block closes
|
|
122
124
|
pending_name = None
|
|
123
|
-
continue
|
|
124
125
|
|
|
125
126
|
else:
|
|
126
|
-
#
|
|
127
|
+
# Inside a fenced code block
|
|
127
128
|
if line.lstrip().startswith(fence):
|
|
128
|
-
#
|
|
129
|
+
# End of block
|
|
129
130
|
in_block = False
|
|
130
131
|
code_text = "\n".join(code_buffer)
|
|
131
132
|
snippet_group = None
|
|
132
|
-
#
|
|
133
|
+
# Continue overrides snippet_name for grouping
|
|
133
134
|
if pending_continue:
|
|
134
135
|
snippet_group = pending_continue
|
|
135
136
|
pending_continue = None
|
|
@@ -141,12 +142,12 @@ def parse_markdown(text: str) -> list[CodeSnippet]:
|
|
|
141
142
|
fixtures=pending_fixtures.copy(),
|
|
142
143
|
group=snippet_group,
|
|
143
144
|
))
|
|
144
|
-
#
|
|
145
|
+
# Reset pending marks after collecting
|
|
145
146
|
pending_marks = [CODEBLOCK_MARK] # Reset to default
|
|
146
147
|
snippet_name = None
|
|
147
148
|
pending_fixtures.clear() # Clear pending fixtures
|
|
148
149
|
else:
|
|
149
|
-
#
|
|
150
|
+
# Collect code lines (dedent by block_indent)
|
|
150
151
|
if line.strip() == "":
|
|
151
152
|
code_buffer.append("")
|
|
152
153
|
else:
|
|
@@ -154,17 +155,27 @@ def parse_markdown(text: str) -> list[CodeSnippet]:
|
|
|
154
155
|
code_buffer.append(line[block_indent:])
|
|
155
156
|
else:
|
|
156
157
|
code_buffer.append(line.lstrip())
|
|
157
|
-
continue
|
|
158
158
|
|
|
159
159
|
return snippets
|
|
160
160
|
|
|
161
161
|
|
|
162
|
-
class MarkdownFile(pytest.
|
|
162
|
+
class MarkdownFile(pytest.Module):
|
|
163
163
|
"""
|
|
164
164
|
Collector for Markdown files, extracting only `test_`-prefixed code
|
|
165
165
|
snippets.
|
|
166
166
|
"""
|
|
167
|
-
|
|
167
|
+
|
|
168
|
+
def _getobj(self) -> types.ModuleType:
|
|
169
|
+
m = types.ModuleType(self.path.stem)
|
|
170
|
+
m.__file__ = str(self.path)
|
|
171
|
+
m.__test__ = False # prevent PyCollector from auto-collecting
|
|
172
|
+
return m
|
|
173
|
+
|
|
174
|
+
def collect(self) -> Generator[pytest.Function, None, None]:
|
|
175
|
+
# Register with fixture manager so module-scoped fixtures can find
|
|
176
|
+
# a pytest.Module parent node (fixes scope resolution when plugins
|
|
177
|
+
# like pytest-recording/langchain-tests define module-scoped fixtures).
|
|
178
|
+
self.session._fixturemanager.parsefactories(self)
|
|
168
179
|
text = self.path.read_text(encoding="utf-8")
|
|
169
180
|
raw = parse_markdown(text)
|
|
170
181
|
config = get_config()
|
|
@@ -278,7 +289,7 @@ class MarkdownFile(pytest.File):
|
|
|
278
289
|
name=sn.name,
|
|
279
290
|
callobj=callobj,
|
|
280
291
|
)
|
|
281
|
-
#
|
|
292
|
+
# Apply any marks (e.g. django_db)
|
|
282
293
|
for m in sn.marks:
|
|
283
294
|
fn.add_marker(getattr(pytest.mark, m))
|
|
284
295
|
yield fn
|
|
@@ -3,6 +3,8 @@ import inspect
|
|
|
3
3
|
import re
|
|
4
4
|
import textwrap
|
|
5
5
|
import traceback
|
|
6
|
+
import types
|
|
7
|
+
from collections.abc import Generator
|
|
6
8
|
from pathlib import Path
|
|
7
9
|
from typing import Optional, Union
|
|
8
10
|
|
|
@@ -134,6 +136,8 @@ def parse_rst(text: str, base_dir: Path) -> list[CodeSnippet]:
|
|
|
134
136
|
fixtures=pending_fixtures.copy(),
|
|
135
137
|
)
|
|
136
138
|
snippets.append(snippet)
|
|
139
|
+
pending_marks = [CODEBLOCK_MARK]
|
|
140
|
+
pending_fixtures.clear()
|
|
137
141
|
|
|
138
142
|
i = j + 1
|
|
139
143
|
continue
|
|
@@ -284,9 +288,20 @@ def parse_rst(text: str, base_dir: Path) -> list[CodeSnippet]:
|
|
|
284
288
|
return snippets
|
|
285
289
|
|
|
286
290
|
|
|
287
|
-
class RSTFile(pytest.
|
|
291
|
+
class RSTFile(pytest.Module):
|
|
288
292
|
"""Collect RST code-block tests as real test functions."""
|
|
289
|
-
|
|
293
|
+
|
|
294
|
+
def _getobj(self) -> types.ModuleType:
|
|
295
|
+
m = types.ModuleType(self.path.stem)
|
|
296
|
+
m.__file__ = str(self.path)
|
|
297
|
+
m.__test__ = False # prevent PyCollector from auto-collecting
|
|
298
|
+
return m
|
|
299
|
+
|
|
300
|
+
def collect(self) -> Generator[pytest.Function, None, None]:
|
|
301
|
+
# Register this node with the fixture manager so that module-scoped
|
|
302
|
+
# fixtures (e.g. vcr_cassette_dir from pytest-recording/langchain-tests)
|
|
303
|
+
# can resolve their scope by walking up to a pytest.Module parent.
|
|
304
|
+
self.session._fixturemanager.parsefactories(self)
|
|
290
305
|
text = self.path.read_text(encoding="utf-8")
|
|
291
306
|
raw = parse_rst(text, self.path)
|
|
292
307
|
config = get_config()
|
{pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock/tests/test_customisation.py
RENAMED
|
@@ -32,7 +32,7 @@ x = 1
|
|
|
32
32
|
assert snippets[0].name == "custom_lang"
|
|
33
33
|
assert "x = 1" in snippets[0].code
|
|
34
34
|
|
|
35
|
-
#
|
|
35
|
+
# ------------------------------------------------------------------------
|
|
36
36
|
|
|
37
37
|
def test_unknown_language_ignored(self):
|
|
38
38
|
"""Test that unknown language fence is ignored."""
|
|
@@ -83,7 +83,7 @@ x = 1
|
|
|
83
83
|
snippets = parse_markdown(text)
|
|
84
84
|
assert len(snippets) == 1
|
|
85
85
|
|
|
86
|
-
#
|
|
86
|
+
# ------------------------------------------------------------------------
|
|
87
87
|
|
|
88
88
|
def test_default_md_extension(self):
|
|
89
89
|
"""Test that .md is always a supported extension."""
|
{pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock/tests/test_integration.py
RENAMED
|
@@ -38,9 +38,9 @@ from ..rst import (
|
|
|
38
38
|
)
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
#
|
|
41
|
+
# ============================================================================
|
|
42
42
|
# Test constants.py
|
|
43
|
-
#
|
|
43
|
+
# ============================================================================
|
|
44
44
|
class TestConstants:
|
|
45
45
|
"""Test constants module values."""
|
|
46
46
|
|
|
@@ -57,9 +57,9 @@ class TestConstants:
|
|
|
57
57
|
assert TEST_PREFIX == "test_"
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
#
|
|
60
|
+
# ============================================================================
|
|
61
61
|
# Test collector.py - CodeSnippet dataclass
|
|
62
|
-
#
|
|
62
|
+
# ============================================================================
|
|
63
63
|
class TestCodeSnippet:
|
|
64
64
|
"""Test CodeSnippet dataclass."""
|
|
65
65
|
|
|
@@ -95,9 +95,9 @@ class TestCodeSnippet:
|
|
|
95
95
|
assert "fixtures" in field_names
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
#
|
|
98
|
+
# ============================================================================
|
|
99
99
|
# Test collector.py - group_snippets function
|
|
100
|
-
#
|
|
100
|
+
# ============================================================================
|
|
101
101
|
class TestGroupSnippets:
|
|
102
102
|
"""Test group_snippets function."""
|
|
103
103
|
|
|
@@ -165,9 +165,9 @@ class TestGroupSnippets:
|
|
|
165
165
|
assert "y=2" in combined[0].code
|
|
166
166
|
|
|
167
167
|
|
|
168
|
-
#
|
|
168
|
+
# ============================================================================
|
|
169
169
|
# Test helpers.py - contains_top_level_await
|
|
170
|
-
#
|
|
170
|
+
# ============================================================================
|
|
171
171
|
class TestContainsTopLevelAwait:
|
|
172
172
|
"""Test contains_top_level_await function."""
|
|
173
173
|
|
|
@@ -194,9 +194,9 @@ class TestContainsTopLevelAwait:
|
|
|
194
194
|
assert contains_top_level_await("def broken(:") is False
|
|
195
195
|
|
|
196
196
|
|
|
197
|
-
#
|
|
197
|
+
# ============================================================================
|
|
198
198
|
# Test helpers.py - wrap_async_code
|
|
199
|
-
#
|
|
199
|
+
# ============================================================================
|
|
200
200
|
class TestWrapAsyncCode:
|
|
201
201
|
"""Test wrap_async_code function."""
|
|
202
202
|
|
|
@@ -222,9 +222,9 @@ class TestWrapAsyncCode:
|
|
|
222
222
|
compile(wrapped, "<test>", "exec")
|
|
223
223
|
|
|
224
224
|
|
|
225
|
-
#
|
|
225
|
+
# ============================================================================
|
|
226
226
|
# Test __init__.py - pytest_collect_file hook
|
|
227
|
-
#
|
|
227
|
+
# ============================================================================
|
|
228
228
|
class TestPytestCollectFile:
|
|
229
229
|
"""Test pytest_collect_file hook function."""
|
|
230
230
|
|
|
@@ -292,9 +292,9 @@ class TestPytestCollectFile:
|
|
|
292
292
|
assert isinstance(result, MarkdownFile)
|
|
293
293
|
|
|
294
294
|
|
|
295
|
-
#
|
|
295
|
+
# ============================================================================
|
|
296
296
|
# Test md.py - parse_markdown function
|
|
297
|
-
#
|
|
297
|
+
# ============================================================================
|
|
298
298
|
class TestParseMarkdown:
|
|
299
299
|
"""Test parse_markdown function."""
|
|
300
300
|
|
|
@@ -310,7 +310,7 @@ x = 1
|
|
|
310
310
|
assert snippets[0].name == "test_simple"
|
|
311
311
|
assert "x = 1" in snippets[0].code
|
|
312
312
|
|
|
313
|
-
#
|
|
313
|
+
# ------------------------------------------------------------------------
|
|
314
314
|
|
|
315
315
|
def test_parse_with_pytestmark(self):
|
|
316
316
|
"""Test the <!-- pytestmark: mark --> directive."""
|
|
@@ -323,7 +323,7 @@ pass
|
|
|
323
323
|
snippets = parse_markdown(text)
|
|
324
324
|
assert "django_db" in snippets[0].marks
|
|
325
325
|
|
|
326
|
-
#
|
|
326
|
+
# ------------------------------------------------------------------------
|
|
327
327
|
|
|
328
328
|
def test_parse_with_pytestfixture(self):
|
|
329
329
|
"""Test the <!-- pytestfixture: name --> directive."""
|
|
@@ -340,7 +340,7 @@ print("hello")
|
|
|
340
340
|
assert "tmp_path" in snippets[0].fixtures
|
|
341
341
|
assert "capsys" in snippets[0].fixtures
|
|
342
342
|
|
|
343
|
-
#
|
|
343
|
+
# ------------------------------------------------------------------------
|
|
344
344
|
|
|
345
345
|
def test_parse_continue_directive(self):
|
|
346
346
|
"""Test the <!-- continue: name --> directive for grouping snippets."""
|
|
@@ -366,7 +366,7 @@ assert y == 2
|
|
|
366
366
|
assert "x = 1" in test_snippets[0].code
|
|
367
367
|
assert "y = x + 1" in test_snippets[0].code
|
|
368
368
|
|
|
369
|
-
#
|
|
369
|
+
# ------------------------------------------------------------------------
|
|
370
370
|
|
|
371
371
|
def test_parse_incremental_continuation(self):
|
|
372
372
|
"""Named continuation blocks produce N cumulative tests."""
|
|
@@ -398,7 +398,7 @@ something = Exception("")
|
|
|
398
398
|
assert 'something = "a"' in grouped[2].code
|
|
399
399
|
assert 'something = Exception("")' in grouped[2].code
|
|
400
400
|
|
|
401
|
-
#
|
|
401
|
+
# ------------------------------------------------------------------------
|
|
402
402
|
|
|
403
403
|
def test_parse_codeblock_name_directive(self):
|
|
404
404
|
"""Test the <!-- codeblock-name: name --> directive."""
|
|
@@ -414,7 +414,7 @@ assert z == 42
|
|
|
414
414
|
assert len(snippets) == 1
|
|
415
415
|
assert snippets[0].name == "test_named"
|
|
416
416
|
|
|
417
|
-
#
|
|
417
|
+
# ------------------------------------------------------------------------
|
|
418
418
|
|
|
419
419
|
def test_parse_py_language(self):
|
|
420
420
|
"""Test markdown with 'py' as language identifier."""
|
|
@@ -427,7 +427,7 @@ x = 1
|
|
|
427
427
|
assert len(snippets) == 1
|
|
428
428
|
assert snippets[0].name == "test_py_lang"
|
|
429
429
|
|
|
430
|
-
#
|
|
430
|
+
# ------------------------------------------------------------------------
|
|
431
431
|
|
|
432
432
|
def test_parse_python3_language(self):
|
|
433
433
|
"""Test markdown with 'python3' as language identifier."""
|
|
@@ -440,7 +440,7 @@ x = 1
|
|
|
440
440
|
assert len(snippets) == 1
|
|
441
441
|
assert snippets[0].name == "test_python3"
|
|
442
442
|
|
|
443
|
-
#
|
|
443
|
+
# ------------------------------------------------------------------------
|
|
444
444
|
|
|
445
445
|
def test_parse_non_python_codeblock_ignored(self):
|
|
446
446
|
"""Test that non-Python code blocks are skipped."""
|
|
@@ -458,7 +458,7 @@ x = 1
|
|
|
458
458
|
assert len(snippets) == 1
|
|
459
459
|
assert snippets[0].name == "test_py"
|
|
460
460
|
|
|
461
|
-
#
|
|
461
|
+
# ------------------------------------------------------------------------
|
|
462
462
|
|
|
463
463
|
def test_parse_name_colon_syntax(self):
|
|
464
464
|
"""Test name= vs name: syntax in fence info string."""
|
|
@@ -470,7 +470,7 @@ x = 1
|
|
|
470
470
|
snippets = parse_markdown(text)
|
|
471
471
|
assert snippets[0].name == "test_colon"
|
|
472
472
|
|
|
473
|
-
#
|
|
473
|
+
# ------------------------------------------------------------------------
|
|
474
474
|
|
|
475
475
|
def test_parse_empty_codeblock(self):
|
|
476
476
|
"""Test parse empty code block."""
|
|
@@ -482,7 +482,7 @@ x = 1
|
|
|
482
482
|
assert len(snippets) == 1
|
|
483
483
|
assert snippets[0].code == ""
|
|
484
484
|
|
|
485
|
-
#
|
|
485
|
+
# ------------------------------------------------------------------------
|
|
486
486
|
|
|
487
487
|
def test_parse_indented_fence(self):
|
|
488
488
|
"""Test fence with indentation."""
|
|
@@ -494,7 +494,7 @@ x = 1
|
|
|
494
494
|
snippets = parse_markdown(text)
|
|
495
495
|
assert len(snippets) == 1
|
|
496
496
|
|
|
497
|
-
#
|
|
497
|
+
# ------------------------------------------------------------------------
|
|
498
498
|
|
|
499
499
|
# TODO: Remove?
|
|
500
500
|
def test_parse_fence_regex_edge_case(self):
|
|
@@ -508,7 +508,7 @@ x = 1
|
|
|
508
508
|
snippets = parse_markdown(text)
|
|
509
509
|
assert len(snippets) == 1
|
|
510
510
|
|
|
511
|
-
#
|
|
511
|
+
# ------------------------------------------------------------------------
|
|
512
512
|
|
|
513
513
|
def test_parse_markdown_mixed_indentation(self):
|
|
514
514
|
"""Test parsing codeblock with mixed indentation levels."""
|
|
@@ -524,7 +524,7 @@ x = 1
|
|
|
524
524
|
# Code should be dedented based on fence indentation
|
|
525
525
|
assert "x = 1" in snippets[0].code
|
|
526
526
|
|
|
527
|
-
#
|
|
527
|
+
# ------------------------------------------------------------------------
|
|
528
528
|
|
|
529
529
|
def test_parse_short_line_in_block(self):
|
|
530
530
|
"""Test code block with line shorter than indent."""
|
|
@@ -541,9 +541,9 @@ y
|
|
|
541
541
|
# The short line 'y' should still be captured
|
|
542
542
|
assert "y" in snippets[0].code or "x = 1" in snippets[0].code
|
|
543
543
|
|
|
544
|
-
#
|
|
544
|
+
# ============================================================================
|
|
545
545
|
# Test rst.py - resolve_literalinclude_path
|
|
546
|
-
#
|
|
546
|
+
# ============================================================================
|
|
547
547
|
class TestResolveLiteralincludePath:
|
|
548
548
|
"""Test resolve_literalinclude_path function."""
|
|
549
549
|
|
|
@@ -584,9 +584,9 @@ class TestResolveLiteralincludePath:
|
|
|
584
584
|
assert result is None
|
|
585
585
|
|
|
586
586
|
|
|
587
|
-
#
|
|
587
|
+
# ============================================================================
|
|
588
588
|
# Test rst.py - get_literalinclude_content
|
|
589
|
-
#
|
|
589
|
+
# ============================================================================
|
|
590
590
|
class TestGetLiteralincludeContent:
|
|
591
591
|
"""Test get_literalinclude_content function."""
|
|
592
592
|
|
|
@@ -605,9 +605,9 @@ class TestGetLiteralincludeContent:
|
|
|
605
605
|
get_literalinclude_content(str(tmp_path / "missing.py"))
|
|
606
606
|
|
|
607
607
|
|
|
608
|
-
#
|
|
608
|
+
# ============================================================================
|
|
609
609
|
# Test rst.py - parse_rst function
|
|
610
|
-
#
|
|
610
|
+
# ============================================================================
|
|
611
611
|
class TestParseRst:
|
|
612
612
|
"""Test parse_rst function."""
|
|
613
613
|
|
|
@@ -623,7 +623,7 @@ class TestParseRst:
|
|
|
623
623
|
assert len(snippets) == 1
|
|
624
624
|
assert snippets[0].name == "test_rst"
|
|
625
625
|
|
|
626
|
-
#
|
|
626
|
+
# ------------------------------------------------------------------------
|
|
627
627
|
|
|
628
628
|
def test_parse_code_directive(self, tmp_path):
|
|
629
629
|
"""Test .. code:: python (alternative to code-block)."""
|
|
@@ -637,7 +637,7 @@ class TestParseRst:
|
|
|
637
637
|
assert len(snippets) == 1
|
|
638
638
|
assert snippets[0].name == "test_code"
|
|
639
639
|
|
|
640
|
-
#
|
|
640
|
+
# ------------------------------------------------------------------------
|
|
641
641
|
|
|
642
642
|
def test_parse_pytestmark(self, tmp_path):
|
|
643
643
|
rst = """
|
|
@@ -651,7 +651,7 @@ class TestParseRst:
|
|
|
651
651
|
snippets = parse_rst(rst, tmp_path)
|
|
652
652
|
assert "django_db" in snippets[0].marks
|
|
653
653
|
|
|
654
|
-
#
|
|
654
|
+
# ------------------------------------------------------------------------
|
|
655
655
|
|
|
656
656
|
def test_parse_pytestfixture(self, tmp_path):
|
|
657
657
|
"""Test the .. pytestfixture: directive."""
|
|
@@ -668,7 +668,7 @@ class TestParseRst:
|
|
|
668
668
|
assert len(snippets) == 1
|
|
669
669
|
assert "tmp_path" in snippets[0].fixtures
|
|
670
670
|
|
|
671
|
-
#
|
|
671
|
+
# ------------------------------------------------------------------------
|
|
672
672
|
|
|
673
673
|
def test_parse_continue_directive(self, tmp_path):
|
|
674
674
|
"""Test the .. continue: directive for grouping RST snippets."""
|
|
@@ -695,7 +695,7 @@ Some text.
|
|
|
695
695
|
assert "a = 10" in test_snippets[0].code
|
|
696
696
|
assert "b = a + 5" in test_snippets[0].code
|
|
697
697
|
|
|
698
|
-
#
|
|
698
|
+
# ------------------------------------------------------------------------
|
|
699
699
|
|
|
700
700
|
def test_parse_codeblock_name(self, tmp_path):
|
|
701
701
|
rst = """
|
|
@@ -708,7 +708,7 @@ Some text.
|
|
|
708
708
|
snippets = parse_rst(rst, tmp_path)
|
|
709
709
|
assert snippets[0].name == "test_named"
|
|
710
710
|
|
|
711
|
-
#
|
|
711
|
+
# ------------------------------------------------------------------------
|
|
712
712
|
|
|
713
713
|
def test_parse_literal_block(self, tmp_path):
|
|
714
714
|
"""Test parsing of literal blocks via :: syntax."""
|
|
@@ -726,7 +726,7 @@ assert result == 3
|
|
|
726
726
|
assert snippets[0].name == "test_literal"
|
|
727
727
|
assert "result = 1 + 2" in snippets[0].code
|
|
728
728
|
|
|
729
|
-
#
|
|
729
|
+
# ------------------------------------------------------------------------
|
|
730
730
|
|
|
731
731
|
def test_parse_rst_continue_in_literal_block(self, tmp_path):
|
|
732
732
|
"""Test continue directive with literal block syntax."""
|
|
@@ -751,7 +751,7 @@ b = 2
|
|
|
751
751
|
matching = [s for s in grouped if s.name == "test_lit_continue"]
|
|
752
752
|
assert len(matching) >= 1
|
|
753
753
|
|
|
754
|
-
#
|
|
754
|
+
# ------------------------------------------------------------------------
|
|
755
755
|
|
|
756
756
|
def test_parse_literalinclude(self, tmp_path):
|
|
757
757
|
"""Test literalinclude directive with test_ name."""
|
|
@@ -766,7 +766,7 @@ b = 2
|
|
|
766
766
|
assert len(snippets) == 1
|
|
767
767
|
assert "def hello():" in snippets[0].code
|
|
768
768
|
|
|
769
|
-
#
|
|
769
|
+
# ------------------------------------------------------------------------
|
|
770
770
|
|
|
771
771
|
def test_parse_literalinclude_no_test_prefix(self, tmp_path):
|
|
772
772
|
"""Test literalinclude without test_ prefix is skipped."""
|
|
@@ -780,7 +780,7 @@ b = 2
|
|
|
780
780
|
# Should be empty because name doesn't start with test_
|
|
781
781
|
assert len(snippets) == 0
|
|
782
782
|
|
|
783
|
-
#
|
|
783
|
+
# ------------------------------------------------------------------------
|
|
784
784
|
|
|
785
785
|
def test_parse_non_python_code_block(self, tmp_path):
|
|
786
786
|
"""Non-python code blocks are skipped."""
|
|
@@ -792,7 +792,7 @@ b = 2
|
|
|
792
792
|
snippets = parse_rst(rst, tmp_path)
|
|
793
793
|
assert len(snippets) == 0
|
|
794
794
|
|
|
795
|
-
#
|
|
795
|
+
# ------------------------------------------------------------------------
|
|
796
796
|
|
|
797
797
|
def test_parse_wrong_indent(self, tmp_path):
|
|
798
798
|
"""Code at wrong indent level."""
|
|
@@ -807,7 +807,7 @@ x = 1
|
|
|
807
807
|
# Should not collect this as a valid snippet
|
|
808
808
|
assert len(snippets) == 0
|
|
809
809
|
|
|
810
|
-
#
|
|
810
|
+
# ------------------------------------------------------------------------
|
|
811
811
|
|
|
812
812
|
def test_parse_literal_codeblock_eof(self, tmp_path):
|
|
813
813
|
"""Test literal block at end of file."""
|
|
@@ -820,7 +820,7 @@ Block::"""
|
|
|
820
820
|
# Should handle gracefully
|
|
821
821
|
assert len(snippets) == 0
|
|
822
822
|
|
|
823
|
-
#
|
|
823
|
+
# ------------------------------------------------------------------------
|
|
824
824
|
|
|
825
825
|
def test_parse_empty_codeblock(self, tmp_path):
|
|
826
826
|
"""Test parsing an empty code block."""
|
|
@@ -833,7 +833,7 @@ Block::"""
|
|
|
833
833
|
# Empty blocks are collected but have no snippets
|
|
834
834
|
assert len(snippets) == 0
|
|
835
835
|
|
|
836
|
-
#
|
|
836
|
+
# ------------------------------------------------------------------------
|
|
837
837
|
|
|
838
838
|
def test_parse_literal_block_empty_line_after(self, tmp_path):
|
|
839
839
|
"""Test literal block with just empty line after (edge case)."""
|
|
@@ -848,13 +848,13 @@ Block::
|
|
|
848
848
|
assert len(snippets) == 0
|
|
849
849
|
|
|
850
850
|
|
|
851
|
-
#
|
|
851
|
+
# ============================================================================
|
|
852
852
|
# Integration tests using pytester - exercises collectors and hook
|
|
853
|
-
#
|
|
853
|
+
# ============================================================================
|
|
854
854
|
|
|
855
|
-
#
|
|
855
|
+
# ----------------------------------------------------------------------------
|
|
856
856
|
# Test RSTFile.collect() method
|
|
857
|
-
#
|
|
857
|
+
# ----------------------------------------------------------------------------
|
|
858
858
|
|
|
859
859
|
class TestMarkdownCollector:
|
|
860
860
|
"""Integration tests for MarkdownFile collector."""
|
|
@@ -876,7 +876,7 @@ assert x == 1
|
|
|
876
876
|
result.assert_outcomes(passed=1)
|
|
877
877
|
assert "test_basic" in result.stdout.str()
|
|
878
878
|
|
|
879
|
-
#
|
|
879
|
+
# ------------------------------------------------------------------------
|
|
880
880
|
|
|
881
881
|
def test_collect_with_fixture(self, pytester_subprocess):
|
|
882
882
|
"""Test that fixtures are properly injected."""
|
|
@@ -892,7 +892,7 @@ assert tmp_path.exists()
|
|
|
892
892
|
result = pytester_subprocess.runpytest("-v", "-p", "no:django")
|
|
893
893
|
result.assert_outcomes(passed=1)
|
|
894
894
|
|
|
895
|
-
#
|
|
895
|
+
# ------------------------------------------------------------------------
|
|
896
896
|
|
|
897
897
|
def test_collect_async_code(self, pytester_subprocess):
|
|
898
898
|
"""Test that async code is automatically wrapped."""
|
|
@@ -908,7 +908,7 @@ await asyncio.sleep(0)
|
|
|
908
908
|
result = pytester_subprocess.runpytest("-v", "-p", "no:django")
|
|
909
909
|
result.assert_outcomes(passed=1)
|
|
910
910
|
|
|
911
|
-
#
|
|
911
|
+
# ------------------------------------------------------------------------
|
|
912
912
|
|
|
913
913
|
def test_syntax_error_reporting(self, pytester_subprocess):
|
|
914
914
|
"""Test that syntax errors in snippets are properly reported."""
|
|
@@ -928,7 +928,7 @@ def broken(:
|
|
|
928
928
|
or "syntax" in result.stdout.str().lower()
|
|
929
929
|
)
|
|
930
930
|
|
|
931
|
-
#
|
|
931
|
+
# ------------------------------------------------------------------------
|
|
932
932
|
|
|
933
933
|
def test_runtime_error_reporting(self, pytester_subprocess):
|
|
934
934
|
"""Test that runtime errors in snippets are properly reported."""
|
|
@@ -944,7 +944,7 @@ raise ValueError("intentional error")
|
|
|
944
944
|
result.assert_outcomes(failed=1)
|
|
945
945
|
assert "ValueError" in result.stdout.str()
|
|
946
946
|
|
|
947
|
-
#
|
|
947
|
+
# ------------------------------------------------------------------------
|
|
948
948
|
|
|
949
949
|
def test_incremental_continue_collects_separate_tests(
|
|
950
950
|
self, pytester_subprocess
|
|
@@ -979,9 +979,9 @@ assert isinstance(something, Exception)
|
|
|
979
979
|
assert "test_step_three" in stdout
|
|
980
980
|
|
|
981
981
|
|
|
982
|
-
#
|
|
982
|
+
# ----------------------------------------------------------------------------
|
|
983
983
|
# Test RSTFile.collect() method
|
|
984
|
-
#
|
|
984
|
+
# ----------------------------------------------------------------------------
|
|
985
985
|
|
|
986
986
|
class TestRSTCollector:
|
|
987
987
|
"""Integration tests for RSTFile collector."""
|
|
@@ -1005,7 +1005,7 @@ Test File
|
|
|
1005
1005
|
result.assert_outcomes(passed=1)
|
|
1006
1006
|
assert "test_rst_basic" in result.stdout.str()
|
|
1007
1007
|
|
|
1008
|
-
#
|
|
1008
|
+
# ------------------------------------------------------------------------
|
|
1009
1009
|
|
|
1010
1010
|
def test_collect_with_fixture(self, pytester_subprocess):
|
|
1011
1011
|
"""Test that RST fixtures are properly injected."""
|
|
@@ -1023,7 +1023,7 @@ Test File
|
|
|
1023
1023
|
result = pytester_subprocess.runpytest("-v", "-p", "no:django")
|
|
1024
1024
|
result.assert_outcomes(passed=1)
|
|
1025
1025
|
|
|
1026
|
-
#
|
|
1026
|
+
# ------------------------------------------------------------------------
|
|
1027
1027
|
|
|
1028
1028
|
def test_collect_async_code(self, pytester_subprocess):
|
|
1029
1029
|
"""Test that RST async code is automatically wrapped."""
|
|
@@ -1040,7 +1040,7 @@ Test File
|
|
|
1040
1040
|
result = pytester_subprocess.runpytest("-v", "-p", "no:django")
|
|
1041
1041
|
result.assert_outcomes(passed=1)
|
|
1042
1042
|
|
|
1043
|
-
#
|
|
1043
|
+
# ------------------------------------------------------------------------
|
|
1044
1044
|
|
|
1045
1045
|
def test_syntax_error_reporting(self, pytester_subprocess):
|
|
1046
1046
|
"""Test that syntax errors in RST snippets are reported."""
|
|
@@ -1058,9 +1058,9 @@ Test File
|
|
|
1058
1058
|
result.assert_outcomes(failed=1)
|
|
1059
1059
|
|
|
1060
1060
|
|
|
1061
|
-
#
|
|
1061
|
+
# ----------------------------------------------------------------------------
|
|
1062
1062
|
# Tests for pytest_collect_file hook dispatch
|
|
1063
|
-
#
|
|
1063
|
+
# ----------------------------------------------------------------------------
|
|
1064
1064
|
|
|
1065
1065
|
|
|
1066
1066
|
class TestPytestCollectFileHook:
|
|
@@ -1081,7 +1081,7 @@ assert True
|
|
|
1081
1081
|
)
|
|
1082
1082
|
assert "test_md_hook" in result.stdout.str()
|
|
1083
1083
|
|
|
1084
|
-
#
|
|
1084
|
+
# ------------------------------------------------------------------------
|
|
1085
1085
|
|
|
1086
1086
|
def test_hook_dispatches_rst(self, pytester_subprocess):
|
|
1087
1087
|
"""Test that .rst files are dispatched to RSTFile."""
|
|
@@ -1099,7 +1099,7 @@ assert True
|
|
|
1099
1099
|
)
|
|
1100
1100
|
assert "test_rst_hook" in result.stdout.str()
|
|
1101
1101
|
|
|
1102
|
-
#
|
|
1102
|
+
# ------------------------------------------------------------------------
|
|
1103
1103
|
|
|
1104
1104
|
def test_hook_ignores_other_files(self, pytester_subprocess):
|
|
1105
1105
|
"""Test that non-.md/.rst files are ignored."""
|
|
@@ -33,9 +33,9 @@ __all__ = (
|
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
#
|
|
36
|
+
# ============================================================================
|
|
37
37
|
# Test Configuration Loading
|
|
38
|
-
#
|
|
38
|
+
# ============================================================================
|
|
39
39
|
class TestConfigLoading:
|
|
40
40
|
"""Test that test_nameless_codeblocks config loads correctly."""
|
|
41
41
|
|
|
@@ -72,9 +72,9 @@ class TestConfigLoading:
|
|
|
72
72
|
assert config.test_nameless_codeblocks is False
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
#
|
|
75
|
+
# ============================================================================
|
|
76
76
|
# Test Auto-naming Logic
|
|
77
|
-
#
|
|
77
|
+
# ============================================================================
|
|
78
78
|
class TestAutoNaming:
|
|
79
79
|
"""Test the auto-naming scheme for nameless code blocks."""
|
|
80
80
|
|
|
@@ -107,9 +107,9 @@ class TestAutoNaming:
|
|
|
107
107
|
assert module_name == "my.doc"
|
|
108
108
|
|
|
109
109
|
|
|
110
|
-
#
|
|
110
|
+
# ============================================================================
|
|
111
111
|
# Test Markdown Collector with Nameless Blocks
|
|
112
|
-
#
|
|
112
|
+
# ============================================================================
|
|
113
113
|
class TestMarkdownNameless:
|
|
114
114
|
"""Test MarkdownFile collector with test_nameless_codeblocks."""
|
|
115
115
|
|
|
@@ -132,7 +132,7 @@ y = 2
|
|
|
132
132
|
assert "x = 1" in snippets[0].code
|
|
133
133
|
assert "y = 2" in snippets[1].code
|
|
134
134
|
|
|
135
|
-
#
|
|
135
|
+
# ------------------------------------------------------------------------
|
|
136
136
|
|
|
137
137
|
def test_parse_markdown_mixed_named_and_nameless(self):
|
|
138
138
|
"""Test parsing mix of named and nameless blocks."""
|
|
@@ -160,7 +160,7 @@ d = 4
|
|
|
160
160
|
assert snippets[2].name == "test_two"
|
|
161
161
|
assert snippets[3].name is None
|
|
162
162
|
|
|
163
|
-
#
|
|
163
|
+
# ------------------------------------------------------------------------
|
|
164
164
|
|
|
165
165
|
def test_collect_nameless_disabled_default(self):
|
|
166
166
|
"""Test that nameless blocks are ignored by default."""
|
|
@@ -208,7 +208,7 @@ y = 2
|
|
|
208
208
|
assert len(tests) == 1
|
|
209
209
|
assert tests[0].name == "test_explicit"
|
|
210
210
|
|
|
211
|
-
#
|
|
211
|
+
# ------------------------------------------------------------------------
|
|
212
212
|
|
|
213
213
|
def test_collect_nameless_enabled(self):
|
|
214
214
|
"""Test that nameless blocks are collected when enabled."""
|
|
@@ -251,7 +251,7 @@ z = 3
|
|
|
251
251
|
assert tests[1].name == "test_test_1"
|
|
252
252
|
assert tests[2].name == "test_test_2"
|
|
253
253
|
|
|
254
|
-
#
|
|
254
|
+
# ------------------------------------------------------------------------
|
|
255
255
|
|
|
256
256
|
def test_auto_naming_preserves_code(self):
|
|
257
257
|
"""Test that auto-naming doesn't modify the code content."""
|
|
@@ -281,7 +281,7 @@ assert original_code == "unchanged"
|
|
|
281
281
|
assert "original_code" in tests[0].code
|
|
282
282
|
assert "unchanged" in tests[0].code
|
|
283
283
|
|
|
284
|
-
#
|
|
284
|
+
# ------------------------------------------------------------------------
|
|
285
285
|
|
|
286
286
|
def test_auto_naming_preserves_marks(self):
|
|
287
287
|
"""Test that auto-naming preserves pytest marks."""
|
|
@@ -310,7 +310,7 @@ user = User.objects.first()
|
|
|
310
310
|
assert len(tests) == 1
|
|
311
311
|
assert "django_db" in tests[0].marks
|
|
312
312
|
|
|
313
|
-
#
|
|
313
|
+
# ------------------------------------------------------------------------
|
|
314
314
|
|
|
315
315
|
def test_codeblock_marks_on_all_blocks(self):
|
|
316
316
|
"""Test that all blocks have default codeblock marks."""
|
|
@@ -331,7 +331,7 @@ assert True
|
|
|
331
331
|
for sn in raw:
|
|
332
332
|
assert CODEBLOCK_MARK in sn.marks
|
|
333
333
|
|
|
334
|
-
#
|
|
334
|
+
# ------------------------------------------------------------------------
|
|
335
335
|
|
|
336
336
|
def test_auto_naming_preserves_fixtures(self):
|
|
337
337
|
"""Test that auto-naming preserves pytest fixtures."""
|
|
@@ -363,9 +363,9 @@ d.mkdir()
|
|
|
363
363
|
assert "capsys" in tests[0].fixtures
|
|
364
364
|
|
|
365
365
|
|
|
366
|
-
#
|
|
366
|
+
# ============================================================================
|
|
367
367
|
# Test RST Collector with Nameless Blocks
|
|
368
|
-
#
|
|
368
|
+
# ============================================================================
|
|
369
369
|
class TestRSTNameless:
|
|
370
370
|
"""Test RSTFile collector with test_nameless_codeblocks."""
|
|
371
371
|
|
|
@@ -388,7 +388,7 @@ class TestRSTNameless:
|
|
|
388
388
|
assert "x = 1" in snippets[0].code
|
|
389
389
|
assert "y = 2" in snippets[1].code
|
|
390
390
|
|
|
391
|
-
#
|
|
391
|
+
# ------------------------------------------------------------------------
|
|
392
392
|
|
|
393
393
|
def test_parse_rst_mixed_named_and_nameless(self, tmp_path):
|
|
394
394
|
"""Test parsing mix of named and nameless blocks."""
|
|
@@ -418,7 +418,7 @@ class TestRSTNameless:
|
|
|
418
418
|
assert snippets[2].name == "test_two"
|
|
419
419
|
assert snippets[3].name is None
|
|
420
420
|
|
|
421
|
-
#
|
|
421
|
+
# ------------------------------------------------------------------------
|
|
422
422
|
|
|
423
423
|
def test_collect_nameless_disabled_default(self, tmp_path):
|
|
424
424
|
"""Test that nameless blocks are ignored by default in RST."""
|
|
@@ -462,7 +462,7 @@ class TestRSTNameless:
|
|
|
462
462
|
assert len(tests) == 1
|
|
463
463
|
assert tests[0].name == "test_explicit"
|
|
464
464
|
|
|
465
|
-
#
|
|
465
|
+
# ------------------------------------------------------------------------
|
|
466
466
|
|
|
467
467
|
def test_collect_nameless_enabled(self, tmp_path):
|
|
468
468
|
"""Test that nameless blocks are collected when enabled in RST."""
|
|
@@ -507,7 +507,7 @@ class TestRSTNameless:
|
|
|
507
507
|
assert tests[1].name == "test_test_1"
|
|
508
508
|
assert tests[2].name == "test_test_2"
|
|
509
509
|
|
|
510
|
-
#
|
|
510
|
+
# ------------------------------------------------------------------------
|
|
511
511
|
|
|
512
512
|
def test_auto_naming_preserves_code_rst(self, tmp_path):
|
|
513
513
|
"""Test that auto-naming doesn't modify the code content in RST."""
|
|
@@ -539,7 +539,7 @@ class TestRSTNameless:
|
|
|
539
539
|
assert "original_code" in tests[0].code
|
|
540
540
|
assert "unchanged" in tests[0].code
|
|
541
541
|
|
|
542
|
-
#
|
|
542
|
+
# ------------------------------------------------------------------------
|
|
543
543
|
|
|
544
544
|
def test_auto_naming_preserves_marks_rst(self, tmp_path):
|
|
545
545
|
"""Test that auto-naming preserves pytest marks in RST."""
|
|
@@ -571,7 +571,7 @@ class TestRSTNameless:
|
|
|
571
571
|
assert len(tests) == 1
|
|
572
572
|
assert "django_db" in tests[0].marks
|
|
573
573
|
|
|
574
|
-
#
|
|
574
|
+
# ------------------------------------------------------------------------
|
|
575
575
|
|
|
576
576
|
def test_codeblock_marks_on_all_blocks_rst(self, tmp_path):
|
|
577
577
|
"""Test that all blocks in RST file have default codeblock mark."""
|
|
@@ -594,7 +594,7 @@ class TestRSTNameless:
|
|
|
594
594
|
for sn in raw:
|
|
595
595
|
assert CODEBLOCK_MARK in sn.marks
|
|
596
596
|
|
|
597
|
-
#
|
|
597
|
+
# ------------------------------------------------------------------------
|
|
598
598
|
|
|
599
599
|
def test_auto_naming_preserves_fixtures_rst(self, tmp_path):
|
|
600
600
|
"""Test that auto-naming preserves pytest fixtures in RST."""
|
|
@@ -629,9 +629,9 @@ class TestRSTNameless:
|
|
|
629
629
|
assert "capsys" in tests[0].fixtures
|
|
630
630
|
|
|
631
631
|
|
|
632
|
-
#
|
|
632
|
+
# ============================================================================
|
|
633
633
|
# Test Integration Scenarios
|
|
634
|
-
#
|
|
634
|
+
# ============================================================================
|
|
635
635
|
@pytest.mark.skip(
|
|
636
636
|
reason="Skip due to pytest 9 py.path.local deprecation issue in hooks"
|
|
637
637
|
)
|
|
@@ -670,7 +670,7 @@ assert z == 3
|
|
|
670
670
|
assert "test_integration_1" in result.stdout.str()
|
|
671
671
|
assert "test_integration_2" in result.stdout.str()
|
|
672
672
|
|
|
673
|
-
#
|
|
673
|
+
# ------------------------------------------------------------------------
|
|
674
674
|
|
|
675
675
|
def test_rst_nameless_integration(self, pytester):
|
|
676
676
|
"""Test nameless blocks work end-to-end in RST."""
|
|
@@ -706,7 +706,7 @@ Test File
|
|
|
706
706
|
assert "test_integration_1" in result.stdout.str()
|
|
707
707
|
assert "test_integration_2" in result.stdout.str()
|
|
708
708
|
|
|
709
|
-
#
|
|
709
|
+
# ------------------------------------------------------------------------
|
|
710
710
|
|
|
711
711
|
def test_nameless_disabled_integration(self, pytester):
|
|
712
712
|
"""Test that nameless blocks are ignored when disabled."""
|
|
@@ -730,7 +730,7 @@ assert y == 2
|
|
|
730
730
|
assert "test_explicit" in result.stdout.str()
|
|
731
731
|
assert "test_default_1" not in result.stdout.str()
|
|
732
732
|
|
|
733
|
-
#
|
|
733
|
+
# ------------------------------------------------------------------------
|
|
734
734
|
|
|
735
735
|
def test_multiple_files_separate_counters(self, pytester):
|
|
736
736
|
"""Test that each file has its own counter."""
|
|
@@ -768,9 +768,9 @@ b = 2
|
|
|
768
768
|
assert "test_file2_2" in result.stdout.str()
|
|
769
769
|
|
|
770
770
|
|
|
771
|
-
#
|
|
771
|
+
# ============================================================================
|
|
772
772
|
# Test Edge Cases
|
|
773
|
-
#
|
|
773
|
+
# ============================================================================
|
|
774
774
|
class TestEdgeCases:
|
|
775
775
|
"""Test edge cases and corner scenarios."""
|
|
776
776
|
|
|
@@ -801,7 +801,7 @@ z = 3
|
|
|
801
801
|
result = pytester.runpytest("-v", "-p", "no:django")
|
|
802
802
|
result.assert_outcomes(passed=3)
|
|
803
803
|
|
|
804
|
-
#
|
|
804
|
+
# ------------------------------------------------------------------------
|
|
805
805
|
|
|
806
806
|
@pytest.mark.skip(
|
|
807
807
|
reason="Skip due to pytest 9 py.path.local deprecation issue in hooks"
|
|
@@ -830,7 +830,7 @@ y = 2
|
|
|
830
830
|
# No auto-generated names
|
|
831
831
|
assert "test_only_named_1" not in result.stdout.str()
|
|
832
832
|
|
|
833
|
-
#
|
|
833
|
+
# ------------------------------------------------------------------------
|
|
834
834
|
|
|
835
835
|
@pytest.mark.skip(
|
|
836
836
|
reason="Skip due to pytest 9 py.path.local deprecation issue in hooks"
|
|
@@ -857,7 +857,7 @@ assert x == 1
|
|
|
857
857
|
# Should have at least the non-empty one
|
|
858
858
|
assert result.ret == 0
|
|
859
859
|
|
|
860
|
-
#
|
|
860
|
+
# ------------------------------------------------------------------------
|
|
861
861
|
|
|
862
862
|
@pytest.mark.skip(
|
|
863
863
|
reason="Skip due to pytest 9 py.path.local deprecation issue in hooks"
|
|
@@ -887,7 +887,7 @@ y = 2
|
|
|
887
887
|
result.assert_outcomes(passed=2)
|
|
888
888
|
# Only Python blocks should be collected
|
|
889
889
|
|
|
890
|
-
#
|
|
890
|
+
# ------------------------------------------------------------------------
|
|
891
891
|
|
|
892
892
|
def test_nameless_with_continue_directive_md(self):
|
|
893
893
|
"""Test nameless blocks with continue directive in Markdown."""
|
|
@@ -916,7 +916,7 @@ assert y == 2
|
|
|
916
916
|
assert "x = 1" in combined[0].code
|
|
917
917
|
assert "y = x + 1" in combined[0].code
|
|
918
918
|
|
|
919
|
-
#
|
|
919
|
+
# ------------------------------------------------------------------------
|
|
920
920
|
|
|
921
921
|
def test_nameless_with_continue_directive_rst(self, tmp_path):
|
|
922
922
|
"""Test nameless blocks with continue directive in RST."""
|
|
@@ -949,7 +949,7 @@ assert y == 2
|
|
|
949
949
|
assert "x = 1" in combined[0].code
|
|
950
950
|
assert "y = x + 1" in combined[0].code
|
|
951
951
|
|
|
952
|
-
#
|
|
952
|
+
# ------------------------------------------------------------------------
|
|
953
953
|
|
|
954
954
|
def test_counter_only_increments_for_nameless(self):
|
|
955
955
|
"""Test that counter only increments for nameless blocks."""
|
|
@@ -1008,7 +1008,7 @@ f = 6
|
|
|
1008
1008
|
assert tests[3].name == "test_test_2"
|
|
1009
1009
|
assert tests[5].name == "test_test_3"
|
|
1010
1010
|
|
|
1011
|
-
#
|
|
1011
|
+
# ------------------------------------------------------------------------
|
|
1012
1012
|
|
|
1013
1013
|
def test_filename_with_special_characters(self):
|
|
1014
1014
|
"""Test auto-naming with special characters in filename."""
|
|
@@ -1018,7 +1018,7 @@ f = 6
|
|
|
1018
1018
|
auto_name = f"test_{module_name}_1"
|
|
1019
1019
|
assert auto_name == "test_my-test_file_1"
|
|
1020
1020
|
|
|
1021
|
-
#
|
|
1021
|
+
# ------------------------------------------------------------------------
|
|
1022
1022
|
|
|
1023
1023
|
@pytest.mark.skip(
|
|
1024
1024
|
reason="Skip due to pytest 9 py.path.local deprecation issue in hooks"
|
|
@@ -1041,7 +1041,7 @@ x = 1
|
|
|
1041
1041
|
# No tests should be collected
|
|
1042
1042
|
assert result.ret == 5 # Exit code 5 = no tests collected
|
|
1043
1043
|
|
|
1044
|
-
#
|
|
1044
|
+
# ------------------------------------------------------------------------
|
|
1045
1045
|
|
|
1046
1046
|
@pytest.mark.skip(
|
|
1047
1047
|
reason="Skip due to pytest 9 py.path.local deprecation issue in hooks"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-codeblock
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.8
|
|
4
4
|
Summary: Pytest plugin to collect and test code blocks in reStructuredText and Markdown files.
|
|
5
5
|
Author-email: Artur Barseghyan <artur.barseghyan@gmail.com>
|
|
6
6
|
Maintainer-email: Artur Barseghyan <artur.barseghyan@gmail.com>
|
|
@@ -48,6 +48,7 @@ Requires-Dist: pytest; extra == "test"
|
|
|
48
48
|
Requires-Dist: pytest-cov; extra == "test"
|
|
49
49
|
Requires-Dist: pytest-django; extra == "test"
|
|
50
50
|
Requires-Dist: respx; extra == "test"
|
|
51
|
+
Requires-Dist: langchain-tests; extra == "test"
|
|
51
52
|
Provides-Extra: docs
|
|
52
53
|
Requires-Dist: sphinx; extra == "docs"
|
|
53
54
|
Requires-Dist: sphinx-autobuild; extra == "docs"
|
|
@@ -78,6 +79,7 @@ pytest-codeblock
|
|
|
78
79
|
.. _openai: https://github.com/openai/openai-python
|
|
79
80
|
.. _Ollama: https://github.com/ollama/ollama
|
|
80
81
|
.. _tomli: https://pypi.org/project/tomli/
|
|
82
|
+
.. _doc8: https://doc8.readthedocs.io/
|
|
81
83
|
|
|
82
84
|
.. Internal references
|
|
83
85
|
|
|
@@ -219,6 +221,11 @@ See `customisation docs`_ for more.
|
|
|
219
221
|
|
|
220
222
|
Usage
|
|
221
223
|
=====
|
|
224
|
+
.. note::
|
|
225
|
+
|
|
226
|
+
It's highly recommended to use `doc8`_ for catching possible markup errors,
|
|
227
|
+
that otherwise would be difficult to spot.
|
|
228
|
+
|
|
222
229
|
reStructruredText usage
|
|
223
230
|
-----------------------
|
|
224
231
|
Any code directive, such as ``.. code-block:: python``, ``.. code:: python``,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{pytest_codeblock-0.5.6 → pytest_codeblock-0.5.8}/src/pytest_codeblock.egg-info/top_level.txt
RENAMED
|
File without changes
|