pytest-codeblock 0.5.5__tar.gz → 0.5.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {pytest_codeblock-0.5.5/src/pytest_codeblock.egg-info → pytest_codeblock-0.5.7}/PKG-INFO +9 -3
  2. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/README.rst +8 -2
  3. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/pyproject.toml +23 -2
  4. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/src/pytest_codeblock/__init__.py +1 -1
  5. pytest_codeblock-0.5.7/src/pytest_codeblock/collector.py +104 -0
  6. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/src/pytest_codeblock/md.py +12 -14
  7. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/src/pytest_codeblock/rst.py +10 -6
  8. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/src/pytest_codeblock/tests/test_customisation.py +2 -2
  9. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/src/pytest_codeblock/tests/test_integration.py +139 -55
  10. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/src/pytest_codeblock/tests/test_nameless_codeblocks.py +38 -38
  11. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/src/pytest_codeblock/tests/test_pytest_codeblock.py +15 -0
  12. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/src/pytest_codeblock/tests/test_pytestrun_marker.py +5 -5
  13. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7/src/pytest_codeblock.egg-info}/PKG-INFO +9 -3
  14. pytest_codeblock-0.5.5/src/pytest_codeblock/collector.py +0 -52
  15. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/LICENSE +0 -0
  16. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/setup.cfg +0 -0
  17. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/src/pytest_codeblock/config.py +0 -0
  18. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/src/pytest_codeblock/constants.py +0 -0
  19. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/src/pytest_codeblock/helpers.py +0 -0
  20. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/src/pytest_codeblock/pytestrun.py +0 -0
  21. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/src/pytest_codeblock/tests/__init__.py +0 -0
  22. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/src/pytest_codeblock.egg-info/SOURCES.txt +0 -0
  23. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/src/pytest_codeblock.egg-info/dependency_links.txt +0 -0
  24. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/src/pytest_codeblock.egg-info/entry_points.txt +0 -0
  25. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/src/pytest_codeblock.egg-info/requires.txt +0 -0
  26. {pytest_codeblock-0.5.5 → pytest_codeblock-0.5.7}/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.5
3
+ Version: 0.5.7
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>
@@ -78,6 +78,7 @@ pytest-codeblock
78
78
  .. _openai: https://github.com/openai/openai-python
79
79
  .. _Ollama: https://github.com/ollama/ollama
80
80
  .. _tomli: https://pypi.org/project/tomli/
81
+ .. _doc8: https://doc8.readthedocs.io/
81
82
 
82
83
  .. Internal references
83
84
 
@@ -146,8 +147,8 @@ Prerequisites
146
147
  Documentation
147
148
  =============
148
149
  - Documentation is available on `Read the Docs`_.
149
- - For `reStructuredText`_, see a dedicated `reStructuredText docs`_.
150
- - For `Markdown`_, see a dedicated `Markdown docs`_.
150
+ - For `reStructuredText`, see a dedicated `reStructuredText docs`_.
151
+ - For `Markdown`, see a dedicated `Markdown docs`_.
151
152
  - Both `reStructuredText docs`_ and `Markdown docs`_ have extensive
152
153
  documentation on `pytest`_ markers and corresponding ``conftest.py`` hooks.
153
154
  - For guidelines on contributing check the `Contributor guidelines`_.
@@ -219,6 +220,11 @@ See `customisation docs`_ for more.
219
220
 
220
221
  Usage
221
222
  =====
223
+ .. note::
224
+
225
+ It's highly recommended to use `doc8`_ for catching possible markup errors,
226
+ that otherwise would be difficult to spot.
227
+
222
228
  reStructruredText usage
223
229
  -----------------------
224
230
  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
 
@@ -83,8 +84,8 @@ Prerequisites
83
84
  Documentation
84
85
  =============
85
86
  - Documentation is available on `Read the Docs`_.
86
- - For `reStructuredText`_, see a dedicated `reStructuredText docs`_.
87
- - For `Markdown`_, see a dedicated `Markdown docs`_.
87
+ - For `reStructuredText`, see a dedicated `reStructuredText docs`_.
88
+ - For `Markdown`, see a dedicated `Markdown docs`_.
88
89
  - Both `reStructuredText docs`_ and `Markdown docs`_ have extensive
89
90
  documentation on `pytest`_ markers and corresponding ``conftest.py`` hooks.
90
91
  - For guidelines on contributing check the `Contributor guidelines`_.
@@ -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"
5
+ version = "0.5.7"
6
6
  requires-python = ">=3.10"
7
7
  dependencies = [
8
8
  "pytest",
@@ -264,14 +264,30 @@ ignore = [
264
264
  "CODE_OF_CONDUCT.rst",
265
265
  "LICENSE",
266
266
  "SECURITY.rst",
267
+ "docs/_implement_pytest_hooks.rst",
267
268
  "docs/changelog.rst",
268
269
  "docs/code_of_conduct.rst",
270
+ "docs/documentation.rst",
271
+ "docs/full-llms.rst",
272
+ "docs/index.rst",
273
+ "docs/llms.rst",
274
+ "docs/package.rst",
269
275
  "docs/security.rst",
270
276
  "docs/source_tree.rst",
271
277
  "docs/source_tree_full.rst",
272
278
  "docs/make.bat",
273
279
  "docs/Makefile",
274
280
  ]
281
+ order = [
282
+ "README.rst",
283
+ "CONTRIBUTING.rst",
284
+ "docs/quick_start_ref.rst",
285
+ "docs/restructured_text.rst",
286
+ "docs/markdown.rst",
287
+ "docs/cheatsheet_restructured_text.rst",
288
+ "docs/cheatsheet_markdown.rst",
289
+ "docs/customisation.rst",
290
+ ]
275
291
 
276
292
  [[tool.sphinx-source-tree.files]]
277
293
  output = "docs/source_tree_full.rst"
@@ -315,13 +331,18 @@ ignore = [
315
331
  "CODE_OF_CONDUCT.rst",
316
332
  "LICENSE",
317
333
  "SECURITY.rst",
334
+ "docs/_implement_pytest_hooks.rst",
318
335
  "docs/changelog.rst",
319
336
  "docs/code_of_conduct.rst",
337
+ "docs/documentation.rst",
338
+ "docs/full-llms.rst",
339
+ "docs/index.rst",
340
+ "docs/llms.rst",
341
+ "docs/package.rst",
320
342
  "docs/security.rst",
321
343
  "docs/source_tree.rst",
322
344
  "docs/source_tree_full.rst",
323
345
  "docs/make.bat",
324
346
  "docs/Makefile",
325
347
  "examples",
326
- "docs",
327
348
  ]
@@ -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.5"
9
+ __version__ = "0.5.7"
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 default 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 (default 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
@@ -103,7 +103,7 @@ def parse_markdown(text: str) -> list[CodeSnippet]:
103
103
  block_indent = indent
104
104
  start_line = idx + 1
105
105
  code_buffer = []
106
- # determine name from info string or pending comment
106
+ # Determine name from info string or pending comment
107
107
  snippet_name = None
108
108
  for token in extra.split():
109
109
  if (
@@ -118,35 +118,34 @@ def parse_markdown(text: str) -> list[CodeSnippet]:
118
118
  break
119
119
  if snippet_name is None:
120
120
  snippet_name = pending_name
121
- # reset pending_name; marks stay until block closes
121
+ # Reset pending_name; marks stay until block closes
122
122
  pending_name = None
123
- continue
124
123
 
125
124
  else:
126
- # inside a fenced code block
125
+ # Inside a fenced code block
127
126
  if line.lstrip().startswith(fence):
128
- # end of block
127
+ # End of block
129
128
  in_block = False
130
129
  code_text = "\n".join(code_buffer)
131
- # continue overrides snippet_name for grouping
130
+ snippet_group = None
131
+ # Continue overrides snippet_name for grouping
132
132
  if pending_continue:
133
- final_name = pending_continue
133
+ snippet_group = pending_continue
134
134
  pending_continue = None
135
- else:
136
- final_name = snippet_name
137
135
  snippets.append(CodeSnippet(
138
- name=final_name,
136
+ name=snippet_name,
139
137
  code=code_text,
140
138
  line=start_line,
141
139
  marks=pending_marks.copy(),
142
140
  fixtures=pending_fixtures.copy(),
141
+ group=snippet_group,
143
142
  ))
144
- # reset pending marks after collecting
143
+ # Reset pending marks after collecting
145
144
  pending_marks = [CODEBLOCK_MARK] # Reset to default
146
145
  snippet_name = None
147
146
  pending_fixtures.clear() # Clear pending fixtures
148
147
  else:
149
- # collect code lines (dedent by block_indent)
148
+ # Collect code lines (dedent by block_indent)
150
149
  if line.strip() == "":
151
150
  code_buffer.append("")
152
151
  else:
@@ -154,7 +153,6 @@ def parse_markdown(text: str) -> list[CodeSnippet]:
154
153
  code_buffer.append(line[block_indent:])
155
154
  else:
156
155
  code_buffer.append(line.lstrip())
157
- continue
158
156
 
159
157
  return snippets
160
158
 
@@ -278,7 +276,7 @@ class MarkdownFile(pytest.File):
278
276
  name=sn.name,
279
277
  callobj=callobj,
280
278
  )
281
- # apply any marks (e.g. django_db)
279
+ # Apply any marks (e.g. django_db)
282
280
  for m in sn.marks:
283
281
  fn.add_marker(getattr(pytest.mark, m))
284
282
  yield fn
@@ -134,6 +134,8 @@ def parse_rst(text: str, base_dir: Path) -> list[CodeSnippet]:
134
134
  fixtures=pending_fixtures.copy(),
135
135
  )
136
136
  snippets.append(snippet)
137
+ pending_marks = [CODEBLOCK_MARK]
138
+ pending_fixtures.clear()
137
139
 
138
140
  i = j + 1
139
141
  continue
@@ -204,12 +206,12 @@ def parse_rst(text: str, base_dir: Path) -> list[CodeSnippet]:
204
206
  k += 1
205
207
  else:
206
208
  break
209
+ sn_group = None
207
210
  # Decide snippet name: continue overrides name_val/pending_name
208
211
  if pending_continue:
209
- sn_name = pending_continue
212
+ sn_group = pending_continue
210
213
  pending_continue = None
211
- else:
212
- sn_name = name_val or pending_name
214
+ sn_name = name_val or pending_name
213
215
  sn_marks = pending_marks.copy()
214
216
  sn_fixtures = pending_fixtures.copy()
215
217
  pending_name = None
@@ -222,6 +224,7 @@ def parse_rst(text: str, base_dir: Path) -> list[CodeSnippet]:
222
224
  line=j + 1,
223
225
  marks=sn_marks,
224
226
  fixtures=sn_fixtures,
227
+ group=sn_group,
225
228
  ))
226
229
 
227
230
  i = k
@@ -235,11 +238,11 @@ def parse_rst(text: str, base_dir: Path) -> list[CodeSnippet]:
235
238
  # --------------------------------------------------------------------
236
239
  if line.rstrip().endswith("::") and pending_name:
237
240
  # Similar override logic
241
+ sn_group = None
238
242
  if pending_continue:
239
- sn_name = pending_continue
243
+ sn_group = pending_continue
240
244
  pending_continue = None
241
- else:
242
- sn_name = pending_name
245
+ sn_name = pending_name
243
246
  sn_marks = pending_marks.copy()
244
247
  sn_fixtures = pending_fixtures.copy()
245
248
  pending_name = None
@@ -273,6 +276,7 @@ def parse_rst(text: str, base_dir: Path) -> list[CodeSnippet]:
273
276
  line=j + 1,
274
277
  marks=sn_marks,
275
278
  fixtures=sn_fixtures,
279
+ group=sn_group,
276
280
  ))
277
281
  i = k
278
282
  continue
@@ -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."""