pytest-codeblock 0.5.5__tar.gz → 0.5.6__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.5/src/pytest_codeblock.egg-info → pytest_codeblock-0.5.6}/PKG-INFO +3 -3
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/README.rst +2 -2
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/pyproject.toml +1 -1
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock/__init__.py +1 -1
- pytest_codeblock-0.5.6/src/pytest_codeblock/collector.py +104 -0
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock/md.py +4 -4
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock/rst.py +8 -6
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock/tests/test_integration.py +84 -0
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock/tests/test_pytest_codeblock.py +15 -0
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock/tests/test_pytestrun_marker.py +5 -5
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6/src/pytest_codeblock.egg-info}/PKG-INFO +3 -3
- pytest_codeblock-0.5.5/src/pytest_codeblock/collector.py +0 -52
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/LICENSE +0 -0
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/setup.cfg +0 -0
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock/config.py +0 -0
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock/constants.py +0 -0
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock/helpers.py +0 -0
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock/pytestrun.py +0 -0
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock/tests/__init__.py +0 -0
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock/tests/test_customisation.py +0 -0
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock/tests/test_nameless_codeblocks.py +0 -0
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock.egg-info/SOURCES.txt +0 -0
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock.egg-info/dependency_links.txt +0 -0
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock.egg-info/entry_points.txt +0 -0
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock.egg-info/requires.txt +0 -0
- {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/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.6
|
|
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>
|
|
@@ -146,8 +146,8 @@ Prerequisites
|
|
|
146
146
|
Documentation
|
|
147
147
|
=============
|
|
148
148
|
- Documentation is available on `Read the Docs`_.
|
|
149
|
-
- For `reStructuredText
|
|
150
|
-
- For `Markdown
|
|
149
|
+
- For `reStructuredText`, see a dedicated `reStructuredText docs`_.
|
|
150
|
+
- For `Markdown`, see a dedicated `Markdown docs`_.
|
|
151
151
|
- Both `reStructuredText docs`_ and `Markdown docs`_ have extensive
|
|
152
152
|
documentation on `pytest`_ markers and corresponding ``conftest.py`` hooks.
|
|
153
153
|
- For guidelines on contributing check the `Contributor guidelines`_.
|
|
@@ -83,8 +83,8 @@ Prerequisites
|
|
|
83
83
|
Documentation
|
|
84
84
|
=============
|
|
85
85
|
- Documentation is available on `Read the Docs`_.
|
|
86
|
-
- For `reStructuredText
|
|
87
|
-
- For `Markdown
|
|
86
|
+
- For `reStructuredText`, see a dedicated `reStructuredText docs`_.
|
|
87
|
+
- For `Markdown`, see a dedicated `Markdown docs`_.
|
|
88
88
|
- Both `reStructuredText docs`_ and `Markdown docs`_ have extensive
|
|
89
89
|
documentation on `pytest`_ markers and corresponding ``conftest.py`` hooks.
|
|
90
90
|
- For guidelines on contributing check the `Contributor guidelines`_.
|
|
@@ -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.6"
|
|
10
10
|
__author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
|
|
11
11
|
__copyright__ = "2025-2026 Artur Barseghyan"
|
|
12
12
|
__license__ = "MIT"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
__author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
|
|
5
|
+
__copyright__ = "2025-2026 Artur Barseghyan"
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
__all__ = (
|
|
8
|
+
"CodeSnippet",
|
|
9
|
+
"group_snippets",
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class CodeSnippet:
|
|
15
|
+
"""Data container for an extracted code snippet."""
|
|
16
|
+
code: str # The code content
|
|
17
|
+
line: int # Starting line number in the source
|
|
18
|
+
name: Optional[str] = None # Identifier for grouping (None if anonymous)
|
|
19
|
+
marks: list[str] = field(default_factory=list)
|
|
20
|
+
# Collected pytest marks (e.g. ['django_db']), parsed from doc comments
|
|
21
|
+
fixtures: list[str] = field(default_factory=list)
|
|
22
|
+
# Collected pytest fixtures (e.g. ['tmp_path']), parsed from doc comments
|
|
23
|
+
group: Optional[str] = None
|
|
24
|
+
# Set by ``continue:`` directives; names the group this snippet belongs to
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def group_snippets(snippets: list[CodeSnippet]) -> list[CodeSnippet]:
|
|
28
|
+
"""
|
|
29
|
+
Combine snippets that share a group key, using one of two modes:
|
|
30
|
+
|
|
31
|
+
- **Merge mode** (default): snippets sharing the same name (no ``group``
|
|
32
|
+
set, or nameless/same-name continuations) are concatenated into a single
|
|
33
|
+
test, accumulating marks and fixtures. This is the original behaviour.
|
|
34
|
+
- **Incremental mode**: when every continuation snippet (``group`` set) in
|
|
35
|
+
a group also carries its own distinct name, emit one test per snippet.
|
|
36
|
+
Each test's code is the cumulative concatenation of all preceding
|
|
37
|
+
snippets plus itself, so each step is exercised in isolation.
|
|
38
|
+
|
|
39
|
+
Unnamed snippets receive unique auto-keys so they are never merged.
|
|
40
|
+
"""
|
|
41
|
+
# Pass 1: bucket each snippet by its group key, preserving insertion order.
|
|
42
|
+
buckets: dict[str, list[CodeSnippet]] = {}
|
|
43
|
+
order: list[str] = []
|
|
44
|
+
anon_count = 0
|
|
45
|
+
|
|
46
|
+
for sn in snippets:
|
|
47
|
+
if sn.group:
|
|
48
|
+
key = sn.group
|
|
49
|
+
elif sn.name:
|
|
50
|
+
key = sn.name
|
|
51
|
+
else:
|
|
52
|
+
anon_count += 1
|
|
53
|
+
key = f"codeblock{anon_count}"
|
|
54
|
+
|
|
55
|
+
if key not in buckets:
|
|
56
|
+
buckets[key] = []
|
|
57
|
+
order.append(key)
|
|
58
|
+
buckets[key].append(sn)
|
|
59
|
+
|
|
60
|
+
# Pass 2: emit merged or incremental snippets per bucket.
|
|
61
|
+
combined: list[CodeSnippet] = []
|
|
62
|
+
|
|
63
|
+
for key in order:
|
|
64
|
+
members = buckets[key]
|
|
65
|
+
continuations = [sn for sn in members if sn.group]
|
|
66
|
+
# Incremental only when every continuation has a distinct own name.
|
|
67
|
+
incremental = continuations and all(
|
|
68
|
+
sn.name and sn.name != key for sn in continuations
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if incremental:
|
|
72
|
+
acc_code = ""
|
|
73
|
+
acc_marks: list[str] = []
|
|
74
|
+
acc_fixtures: list[str] = []
|
|
75
|
+
for sn in members:
|
|
76
|
+
acc_code = acc_code + "\n" + sn.code if acc_code else sn.code
|
|
77
|
+
acc_marks.extend(sn.marks)
|
|
78
|
+
acc_fixtures.extend(sn.fixtures)
|
|
79
|
+
combined.append(CodeSnippet(
|
|
80
|
+
name=sn.name,
|
|
81
|
+
code=acc_code,
|
|
82
|
+
line=sn.line,
|
|
83
|
+
marks=list(acc_marks),
|
|
84
|
+
fixtures=list(acc_fixtures),
|
|
85
|
+
))
|
|
86
|
+
else:
|
|
87
|
+
# Merge mode (original behaviour).
|
|
88
|
+
first = members[0]
|
|
89
|
+
merged_marks = list(first.marks)
|
|
90
|
+
merged_fixtures = list(first.fixtures)
|
|
91
|
+
merged_code = first.code
|
|
92
|
+
for sn in members[1:]:
|
|
93
|
+
merged_code += "\n" + sn.code
|
|
94
|
+
merged_marks.extend(sn.marks)
|
|
95
|
+
merged_fixtures.extend(sn.fixtures)
|
|
96
|
+
combined.append(CodeSnippet(
|
|
97
|
+
name=first.name,
|
|
98
|
+
code=merged_code,
|
|
99
|
+
line=first.line,
|
|
100
|
+
marks=merged_marks,
|
|
101
|
+
fixtures=merged_fixtures,
|
|
102
|
+
))
|
|
103
|
+
|
|
104
|
+
return combined
|
|
@@ -128,18 +128,18 @@ def parse_markdown(text: str) -> list[CodeSnippet]:
|
|
|
128
128
|
# end of block
|
|
129
129
|
in_block = False
|
|
130
130
|
code_text = "\n".join(code_buffer)
|
|
131
|
+
snippet_group = None
|
|
131
132
|
# continue overrides snippet_name for grouping
|
|
132
133
|
if pending_continue:
|
|
133
|
-
|
|
134
|
+
snippet_group = pending_continue
|
|
134
135
|
pending_continue = None
|
|
135
|
-
else:
|
|
136
|
-
final_name = snippet_name
|
|
137
136
|
snippets.append(CodeSnippet(
|
|
138
|
-
name=
|
|
137
|
+
name=snippet_name,
|
|
139
138
|
code=code_text,
|
|
140
139
|
line=start_line,
|
|
141
140
|
marks=pending_marks.copy(),
|
|
142
141
|
fixtures=pending_fixtures.copy(),
|
|
142
|
+
group=snippet_group,
|
|
143
143
|
))
|
|
144
144
|
# reset pending marks after collecting
|
|
145
145
|
pending_marks = [CODEBLOCK_MARK] # Reset to default
|
|
@@ -204,12 +204,12 @@ def parse_rst(text: str, base_dir: Path) -> list[CodeSnippet]:
|
|
|
204
204
|
k += 1
|
|
205
205
|
else:
|
|
206
206
|
break
|
|
207
|
+
sn_group = None
|
|
207
208
|
# Decide snippet name: continue overrides name_val/pending_name
|
|
208
209
|
if pending_continue:
|
|
209
|
-
|
|
210
|
+
sn_group = pending_continue
|
|
210
211
|
pending_continue = None
|
|
211
|
-
|
|
212
|
-
sn_name = name_val or pending_name
|
|
212
|
+
sn_name = name_val or pending_name
|
|
213
213
|
sn_marks = pending_marks.copy()
|
|
214
214
|
sn_fixtures = pending_fixtures.copy()
|
|
215
215
|
pending_name = None
|
|
@@ -222,6 +222,7 @@ def parse_rst(text: str, base_dir: Path) -> list[CodeSnippet]:
|
|
|
222
222
|
line=j + 1,
|
|
223
223
|
marks=sn_marks,
|
|
224
224
|
fixtures=sn_fixtures,
|
|
225
|
+
group=sn_group,
|
|
225
226
|
))
|
|
226
227
|
|
|
227
228
|
i = k
|
|
@@ -235,11 +236,11 @@ def parse_rst(text: str, base_dir: Path) -> list[CodeSnippet]:
|
|
|
235
236
|
# --------------------------------------------------------------------
|
|
236
237
|
if line.rstrip().endswith("::") and pending_name:
|
|
237
238
|
# Similar override logic
|
|
239
|
+
sn_group = None
|
|
238
240
|
if pending_continue:
|
|
239
|
-
|
|
241
|
+
sn_group = pending_continue
|
|
240
242
|
pending_continue = None
|
|
241
|
-
|
|
242
|
-
sn_name = pending_name
|
|
243
|
+
sn_name = pending_name
|
|
243
244
|
sn_marks = pending_marks.copy()
|
|
244
245
|
sn_fixtures = pending_fixtures.copy()
|
|
245
246
|
pending_name = None
|
|
@@ -273,6 +274,7 @@ def parse_rst(text: str, base_dir: Path) -> list[CodeSnippet]:
|
|
|
273
274
|
line=j + 1,
|
|
274
275
|
marks=sn_marks,
|
|
275
276
|
fixtures=sn_fixtures,
|
|
277
|
+
group=sn_group,
|
|
276
278
|
))
|
|
277
279
|
i = k
|
|
278
280
|
continue
|
{pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock/tests/test_integration.py
RENAMED
|
@@ -368,6 +368,38 @@ assert y == 2
|
|
|
368
368
|
|
|
369
369
|
# -------------------------------------------------------------------------
|
|
370
370
|
|
|
371
|
+
def test_parse_incremental_continuation(self):
|
|
372
|
+
"""Named continuation blocks produce N cumulative tests."""
|
|
373
|
+
text = """
|
|
374
|
+
```python name=test_something
|
|
375
|
+
something = 1
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
<!-- continue: test_something -->
|
|
379
|
+
```python name=test_something_2
|
|
380
|
+
something = "a"
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
<!-- continue: test_something -->
|
|
384
|
+
```python name=test_something_3
|
|
385
|
+
something = Exception("")
|
|
386
|
+
```
|
|
387
|
+
"""
|
|
388
|
+
snippets = parse_markdown(text)
|
|
389
|
+
grouped = group_snippets(snippets)
|
|
390
|
+
assert len(grouped) == 3
|
|
391
|
+
assert grouped[0].name == "test_something"
|
|
392
|
+
assert grouped[0].code.strip() == "something = 1"
|
|
393
|
+
assert grouped[1].name == "test_something_2"
|
|
394
|
+
assert "something = 1" in grouped[1].code
|
|
395
|
+
assert 'something = "a"' in grouped[1].code
|
|
396
|
+
assert grouped[2].name == "test_something_3"
|
|
397
|
+
assert "something = 1" in grouped[2].code
|
|
398
|
+
assert 'something = "a"' in grouped[2].code
|
|
399
|
+
assert 'something = Exception("")' in grouped[2].code
|
|
400
|
+
|
|
401
|
+
# -------------------------------------------------------------------------
|
|
402
|
+
|
|
371
403
|
def test_parse_codeblock_name_directive(self):
|
|
372
404
|
"""Test the <!-- codeblock-name: name --> directive."""
|
|
373
405
|
text = """
|
|
@@ -844,6 +876,8 @@ assert x == 1
|
|
|
844
876
|
result.assert_outcomes(passed=1)
|
|
845
877
|
assert "test_basic" in result.stdout.str()
|
|
846
878
|
|
|
879
|
+
# -------------------------------------------------------------------------
|
|
880
|
+
|
|
847
881
|
def test_collect_with_fixture(self, pytester_subprocess):
|
|
848
882
|
"""Test that fixtures are properly injected."""
|
|
849
883
|
pytester_subprocess.makefile(
|
|
@@ -858,6 +892,8 @@ assert tmp_path.exists()
|
|
|
858
892
|
result = pytester_subprocess.runpytest("-v", "-p", "no:django")
|
|
859
893
|
result.assert_outcomes(passed=1)
|
|
860
894
|
|
|
895
|
+
# -------------------------------------------------------------------------
|
|
896
|
+
|
|
861
897
|
def test_collect_async_code(self, pytester_subprocess):
|
|
862
898
|
"""Test that async code is automatically wrapped."""
|
|
863
899
|
pytester_subprocess.makefile(
|
|
@@ -872,6 +908,8 @@ await asyncio.sleep(0)
|
|
|
872
908
|
result = pytester_subprocess.runpytest("-v", "-p", "no:django")
|
|
873
909
|
result.assert_outcomes(passed=1)
|
|
874
910
|
|
|
911
|
+
# -------------------------------------------------------------------------
|
|
912
|
+
|
|
875
913
|
def test_syntax_error_reporting(self, pytester_subprocess):
|
|
876
914
|
"""Test that syntax errors in snippets are properly reported."""
|
|
877
915
|
pytester_subprocess.makefile(
|
|
@@ -890,6 +928,8 @@ def broken(:
|
|
|
890
928
|
or "syntax" in result.stdout.str().lower()
|
|
891
929
|
)
|
|
892
930
|
|
|
931
|
+
# -------------------------------------------------------------------------
|
|
932
|
+
|
|
893
933
|
def test_runtime_error_reporting(self, pytester_subprocess):
|
|
894
934
|
"""Test that runtime errors in snippets are properly reported."""
|
|
895
935
|
pytester_subprocess.makefile(
|
|
@@ -904,6 +944,40 @@ raise ValueError("intentional error")
|
|
|
904
944
|
result.assert_outcomes(failed=1)
|
|
905
945
|
assert "ValueError" in result.stdout.str()
|
|
906
946
|
|
|
947
|
+
# -------------------------------------------------------------------------
|
|
948
|
+
|
|
949
|
+
def test_incremental_continue_collects_separate_tests(
|
|
950
|
+
self, pytester_subprocess
|
|
951
|
+
):
|
|
952
|
+
"""Each named continuation block becomes its own cumulative test."""
|
|
953
|
+
pytester_subprocess.makefile(
|
|
954
|
+
".md",
|
|
955
|
+
test_incremental="""
|
|
956
|
+
```python name=test_step_one
|
|
957
|
+
something = 1
|
|
958
|
+
assert something == 1
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
<!-- continue: test_step_one -->
|
|
962
|
+
```python name=test_step_two
|
|
963
|
+
something = "a"
|
|
964
|
+
assert something == "a"
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
<!-- continue: test_step_one -->
|
|
968
|
+
```python name=test_step_three
|
|
969
|
+
something = Exception("")
|
|
970
|
+
assert isinstance(something, Exception)
|
|
971
|
+
```
|
|
972
|
+
""",
|
|
973
|
+
)
|
|
974
|
+
result = pytester_subprocess.runpytest("-v", "-p", "no:django")
|
|
975
|
+
result.assert_outcomes(passed=3)
|
|
976
|
+
stdout = result.stdout.str()
|
|
977
|
+
assert "test_step_one" in stdout
|
|
978
|
+
assert "test_step_two" in stdout
|
|
979
|
+
assert "test_step_three" in stdout
|
|
980
|
+
|
|
907
981
|
|
|
908
982
|
# -----------------------------------------------------------------------------
|
|
909
983
|
# Test RSTFile.collect() method
|
|
@@ -931,6 +1005,8 @@ Test File
|
|
|
931
1005
|
result.assert_outcomes(passed=1)
|
|
932
1006
|
assert "test_rst_basic" in result.stdout.str()
|
|
933
1007
|
|
|
1008
|
+
# -------------------------------------------------------------------------
|
|
1009
|
+
|
|
934
1010
|
def test_collect_with_fixture(self, pytester_subprocess):
|
|
935
1011
|
"""Test that RST fixtures are properly injected."""
|
|
936
1012
|
pytester_subprocess.makefile(
|
|
@@ -947,6 +1023,8 @@ Test File
|
|
|
947
1023
|
result = pytester_subprocess.runpytest("-v", "-p", "no:django")
|
|
948
1024
|
result.assert_outcomes(passed=1)
|
|
949
1025
|
|
|
1026
|
+
# -------------------------------------------------------------------------
|
|
1027
|
+
|
|
950
1028
|
def test_collect_async_code(self, pytester_subprocess):
|
|
951
1029
|
"""Test that RST async code is automatically wrapped."""
|
|
952
1030
|
pytester_subprocess.makefile(
|
|
@@ -962,6 +1040,8 @@ Test File
|
|
|
962
1040
|
result = pytester_subprocess.runpytest("-v", "-p", "no:django")
|
|
963
1041
|
result.assert_outcomes(passed=1)
|
|
964
1042
|
|
|
1043
|
+
# -------------------------------------------------------------------------
|
|
1044
|
+
|
|
965
1045
|
def test_syntax_error_reporting(self, pytester_subprocess):
|
|
966
1046
|
"""Test that syntax errors in RST snippets are reported."""
|
|
967
1047
|
pytester_subprocess.makefile(
|
|
@@ -1001,6 +1081,8 @@ assert True
|
|
|
1001
1081
|
)
|
|
1002
1082
|
assert "test_md_hook" in result.stdout.str()
|
|
1003
1083
|
|
|
1084
|
+
# -------------------------------------------------------------------------
|
|
1085
|
+
|
|
1004
1086
|
def test_hook_dispatches_rst(self, pytester_subprocess):
|
|
1005
1087
|
"""Test that .rst files are dispatched to RSTFile."""
|
|
1006
1088
|
pytester_subprocess.makefile(
|
|
@@ -1017,6 +1099,8 @@ assert True
|
|
|
1017
1099
|
)
|
|
1018
1100
|
assert "test_rst_hook" in result.stdout.str()
|
|
1019
1101
|
|
|
1102
|
+
# -------------------------------------------------------------------------
|
|
1103
|
+
|
|
1020
1104
|
def test_hook_ignores_other_files(self, pytester_subprocess):
|
|
1021
1105
|
"""Test that non-.md/.rst files are ignored."""
|
|
1022
1106
|
pytester_subprocess.makefile(".txt", notes="Some notes")
|
|
@@ -36,6 +36,21 @@ def test_group_snippets_merges_named():
|
|
|
36
36
|
assert "m" in cs.marks
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
def test_group_snippets_incremental():
|
|
40
|
+
# Continuation snippets with distinct own names produce N cumulative tests.
|
|
41
|
+
sn1 = CodeSnippet(name="test_root", code="a=1", line=1)
|
|
42
|
+
sn2 = CodeSnippet(name="test_step2", code="b=2", line=5, group="test_root")
|
|
43
|
+
sn3 = CodeSnippet(name="test_step3", code="c=3", line=9, group="test_root")
|
|
44
|
+
combined = group_snippets([sn1, sn2, sn3])
|
|
45
|
+
assert len(combined) == 3
|
|
46
|
+
assert combined[0].name == "test_root"
|
|
47
|
+
assert combined[1].name == "test_step2"
|
|
48
|
+
assert combined[2].name == "test_step3"
|
|
49
|
+
assert combined[0].code == "a=1"
|
|
50
|
+
assert combined[1].code == "a=1\nb=2"
|
|
51
|
+
assert combined[2].code == "a=1\nb=2\nc=3"
|
|
52
|
+
|
|
53
|
+
|
|
39
54
|
def test_group_snippets_different_names():
|
|
40
55
|
# Snippets with different names are not grouped
|
|
41
56
|
sn1 = CodeSnippet(name="foo", code="x=1", line=1)
|
|
@@ -24,9 +24,9 @@ __all__ = (
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
#
|
|
27
|
+
# ============================================================================
|
|
28
28
|
# Test that pytestrun mark is parsed correctly from MD and RST sources
|
|
29
|
-
#
|
|
29
|
+
# ============================================================================
|
|
30
30
|
class TestPytestrunMarkParsing:
|
|
31
31
|
"""Test that the pytestrun mark is captured during parsing."""
|
|
32
32
|
|
|
@@ -93,9 +93,9 @@ class TestPytestrunMarkParsing:
|
|
|
93
93
|
assert PYTESTRUN_MARK not in snippets[0].marks
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
#
|
|
96
|
+
# ============================================================================
|
|
97
97
|
# Test run_pytest_style_code directly
|
|
98
|
-
#
|
|
98
|
+
# ============================================================================
|
|
99
99
|
class TestRunPytestStyleCode:
|
|
100
100
|
"""Unit tests for run_pytest_style_code helper."""
|
|
101
101
|
|
|
@@ -295,7 +295,7 @@ class TestRunPytestStyleCode:
|
|
|
295
295
|
)
|
|
296
296
|
|
|
297
297
|
def test_empty_code_raises_no_tests_collected(self, tmp_path):
|
|
298
|
-
"""
|
|
298
|
+
"""Empty block (no test functions) should fail with non-zero exit."""
|
|
299
299
|
code = "# no tests here\nx = 1\n"
|
|
300
300
|
with pytest.raises(AssertionError):
|
|
301
301
|
run_pytest_style_code(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-codeblock
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.6
|
|
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>
|
|
@@ -146,8 +146,8 @@ Prerequisites
|
|
|
146
146
|
Documentation
|
|
147
147
|
=============
|
|
148
148
|
- Documentation is available on `Read the Docs`_.
|
|
149
|
-
- For `reStructuredText
|
|
150
|
-
- For `Markdown
|
|
149
|
+
- For `reStructuredText`, see a dedicated `reStructuredText docs`_.
|
|
150
|
+
- For `Markdown`, see a dedicated `Markdown docs`_.
|
|
151
151
|
- Both `reStructuredText docs`_ and `Markdown docs`_ have extensive
|
|
152
152
|
documentation on `pytest`_ markers and corresponding ``conftest.py`` hooks.
|
|
153
153
|
- For guidelines on contributing check the `Contributor guidelines`_.
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass, field
|
|
2
|
-
from typing import Optional
|
|
3
|
-
|
|
4
|
-
__author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
|
|
5
|
-
__copyright__ = "2025-2026 Artur Barseghyan"
|
|
6
|
-
__license__ = "MIT"
|
|
7
|
-
__all__ = (
|
|
8
|
-
"CodeSnippet",
|
|
9
|
-
"group_snippets",
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@dataclass
|
|
14
|
-
class CodeSnippet:
|
|
15
|
-
"""Data container for an extracted code snippet."""
|
|
16
|
-
code: str # The code content
|
|
17
|
-
line: int # Starting line number in the source
|
|
18
|
-
name: Optional[str] = None # Identifier for grouping (None if anonymous)
|
|
19
|
-
marks: list[str] = field(default_factory=list)
|
|
20
|
-
# Collected pytest marks (e.g. ['django_db']), parsed from doc comments
|
|
21
|
-
fixtures: list[str] = field(default_factory=list)
|
|
22
|
-
# Collected pytest fixtures (e.g. ['tmp_path']), parsed from doc comments
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def group_snippets(snippets: list[CodeSnippet]) -> list[CodeSnippet]:
|
|
26
|
-
"""
|
|
27
|
-
Merge snippets with the same name into one CodeSnippet,
|
|
28
|
-
concatenating their code and accumulating marks.
|
|
29
|
-
Unnamed snippets get unique auto-names.
|
|
30
|
-
"""
|
|
31
|
-
combined: list[CodeSnippet] = []
|
|
32
|
-
seen: dict[str, CodeSnippet] = {}
|
|
33
|
-
anon_count = 0
|
|
34
|
-
|
|
35
|
-
for sn in snippets:
|
|
36
|
-
key = sn.name
|
|
37
|
-
if not key:
|
|
38
|
-
anon_count += 1
|
|
39
|
-
key = f"codeblock{anon_count}"
|
|
40
|
-
|
|
41
|
-
if key in seen:
|
|
42
|
-
seen_sn = seen[key]
|
|
43
|
-
seen_sn.code += "\n" + sn.code
|
|
44
|
-
seen_sn.marks.extend(sn.marks)
|
|
45
|
-
seen_sn.fixtures.extend(sn.fixtures)
|
|
46
|
-
else:
|
|
47
|
-
sn.marks = list(sn.marks) # copy
|
|
48
|
-
sn.fixtures = list(sn.fixtures) # copy
|
|
49
|
-
seen[key] = sn
|
|
50
|
-
combined.append(sn)
|
|
51
|
-
|
|
52
|
-
return combined
|
|
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.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock/tests/test_customisation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock.egg-info/requires.txt
RENAMED
|
File without changes
|
{pytest_codeblock-0.5.5 → pytest_codeblock-0.5.6}/src/pytest_codeblock.egg-info/top_level.txt
RENAMED
|
File without changes
|