pytest-codeblock 0.5.2__tar.gz → 0.5.4__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 (25) hide show
  1. {pytest_codeblock-0.5.2/src/pytest_codeblock.egg-info → pytest_codeblock-0.5.4}/PKG-INFO +1 -1
  2. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/pyproject.toml +55 -1
  3. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/src/pytest_codeblock/__init__.py +8 -2
  4. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/src/pytest_codeblock/constants.py +6 -0
  5. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/src/pytest_codeblock/md.py +19 -4
  6. pytest_codeblock-0.5.4/src/pytest_codeblock/pytestrun.py +63 -0
  7. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/src/pytest_codeblock/rst.py +20 -7
  8. pytest_codeblock-0.5.4/src/pytest_codeblock/tests/test_pytestrun_marker.py +0 -0
  9. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4/src/pytest_codeblock.egg-info}/PKG-INFO +1 -1
  10. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/src/pytest_codeblock.egg-info/SOURCES.txt +3 -1
  11. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/LICENSE +0 -0
  12. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/README.rst +0 -0
  13. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/setup.cfg +0 -0
  14. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/src/pytest_codeblock/collector.py +0 -0
  15. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/src/pytest_codeblock/config.py +0 -0
  16. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/src/pytest_codeblock/helpers.py +0 -0
  17. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/src/pytest_codeblock/tests/__init__.py +0 -0
  18. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/src/pytest_codeblock/tests/test_customisation.py +0 -0
  19. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/src/pytest_codeblock/tests/test_integration.py +0 -0
  20. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/src/pytest_codeblock/tests/test_nameless_codeblocks.py +0 -0
  21. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/src/pytest_codeblock/tests/test_pytest_codeblock.py +0 -0
  22. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/src/pytest_codeblock.egg-info/dependency_links.txt +0 -0
  23. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/src/pytest_codeblock.egg-info/entry_points.txt +0 -0
  24. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/src/pytest_codeblock.egg-info/requires.txt +0 -0
  25. {pytest_codeblock-0.5.2 → pytest_codeblock-0.5.4}/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.2
3
+ Version: 0.5.4
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>
@@ -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.2"
5
+ version = "0.5.4"
6
6
  requires-python = ">=3.10"
7
7
  dependencies = [
8
8
  "pytest",
@@ -268,6 +268,60 @@ ignore = [
268
268
  "docs/code_of_conduct.rst",
269
269
  "docs/security.rst",
270
270
  "docs/source_tree.rst",
271
+ "docs/source_tree_full.rst",
271
272
  "docs/make.bat",
272
273
  "docs/Makefile",
273
274
  ]
275
+
276
+ [[tool.sphinx-source-tree.files]]
277
+ output = "docs/source_tree_full.rst"
278
+ title = "Full project source-tree"
279
+
280
+ [[tool.sphinx-source-tree.files]]
281
+ output = "docs/source_tree.rst"
282
+ title = "Project source-tree"
283
+ ignore = [
284
+ "__pycache__",
285
+ "*.pyc",
286
+ "*.pyo",
287
+ "*.py,cover",
288
+ ".git",
289
+ ".hg",
290
+ ".svn",
291
+ ".tox",
292
+ ".nox",
293
+ ".venv",
294
+ "venv",
295
+ "env",
296
+ "*.egg-info",
297
+ "dist",
298
+ "build",
299
+ "node_modules",
300
+ ".mypy_cache",
301
+ ".pytest_cache",
302
+ ".coverage",
303
+ "htmlcov",
304
+ ".idea",
305
+ ".vscode",
306
+ ".DS_Store",
307
+ "Thumbs.db",
308
+ ".ruff_cache",
309
+ ".coverage.*",
310
+ ".secrets.baseline",
311
+ ".pre-commit-config.yaml",
312
+ ".pre-commit-hooks.yaml",
313
+ ".readthedocs.yaml",
314
+ "CHANGELOG.rst",
315
+ "CODE_OF_CONDUCT.rst",
316
+ "LICENSE",
317
+ "SECURITY.rst",
318
+ "docs/changelog.rst",
319
+ "docs/code_of_conduct.rst",
320
+ "docs/security.rst",
321
+ "docs/source_tree.rst",
322
+ "docs/source_tree_full.rst",
323
+ "docs/make.bat",
324
+ "docs/Makefile",
325
+ "examples",
326
+ "docs",
327
+ ]
@@ -1,12 +1,12 @@
1
1
  from pathlib import Path
2
2
 
3
3
  from .config import get_config
4
- from .constants import CODEBLOCK_MARK
4
+ from .constants import CODEBLOCK_MARK, PYTESTRUN_MARK
5
5
  from .md import MarkdownFile
6
6
  from .rst import RSTFile
7
7
 
8
8
  __title__ = "pytest-codeblock"
9
- __version__ = "0.5.2"
9
+ __version__ = "0.5.4"
10
10
  __author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
11
11
  __copyright__ = "2025-2026 Artur Barseghyan"
12
12
  __license__ = "MIT"
@@ -42,3 +42,9 @@ def pytest_configure(config):
42
42
  "markers",
43
43
  f"{CODEBLOCK_MARK}: pytest-codeblock markers (auto-registered)",
44
44
  )
45
+ # Only register if not already present
46
+ if PYTESTRUN_MARK not in marker_names:
47
+ config.addinivalue_line(
48
+ "markers",
49
+ f"{PYTESTRUN_MARK}: pytest-codeblock markers (auto-registered)",
50
+ )
@@ -4,6 +4,7 @@ __license__ = "MIT"
4
4
  __all__ = (
5
5
  "CODEBLOCK_MARK",
6
6
  "DJANGO_DB_MARKS",
7
+ "PYTESTRUN_MARK",
7
8
  "TEST_PREFIX",
8
9
  )
9
10
 
@@ -16,3 +17,8 @@ DJANGO_DB_MARKS = {
16
17
  TEST_PREFIX = "test_"
17
18
 
18
19
  CODEBLOCK_MARK = "codeblock"
20
+
21
+ # When this mark is present on a code block, the plugin will exec() the block
22
+ # and then discover and run any Test* classes / test_* functions found in it,
23
+ # rather than treating the whole block as a single test body.
24
+ PYTESTRUN_MARK = "pytestrun"
@@ -9,8 +9,14 @@ import pytest
9
9
 
10
10
  from .collector import CodeSnippet, group_snippets
11
11
  from .config import get_config
12
- from .constants import CODEBLOCK_MARK, DJANGO_DB_MARKS, TEST_PREFIX
12
+ from .constants import (
13
+ CODEBLOCK_MARK,
14
+ DJANGO_DB_MARKS,
15
+ PYTESTRUN_MARK,
16
+ TEST_PREFIX,
17
+ )
13
18
  from .helpers import contains_top_level_await, wrap_async_code
19
+ from .pytestrun import run_pytest_style_code
14
20
 
15
21
  __author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
16
22
  __copyright__ = "2025-2026 Artur Barseghyan"
@@ -25,14 +31,12 @@ def parse_markdown(text: str) -> list[CodeSnippet]:
25
31
  """
26
32
  Parse Markdown text and extract Python code snippets as CodeSnippet
27
33
  objects.
28
-
29
34
  Supports:
30
35
  - <!-- pytestmark: <mark> --> comments immediately before a code fence
31
36
  - <!-- codeblock-name: <name> --> comments for naming
32
37
  - <!-- continue: <name> --> comments for grouping with a named snippet
33
38
  - Fenced code blocks with ```python (and optional name=<name> in the
34
39
  info string)
35
-
36
40
  Captures each snippet's name, code, starting line, and any pytest marks.
37
41
  """
38
42
  config = get_config()
@@ -195,6 +199,7 @@ class MarkdownFile(pytest.File):
195
199
  # Bind the values we need so we don't close over `sn` itself
196
200
  _sn_name = sn.name
197
201
  _fpath = str(self.path)
202
+ _is_pytestrun = PYTESTRUN_MARK in sn.marks
198
203
 
199
204
  # Build list of fixture names requested by this snippet
200
205
  _fixture_names: list[str] = list(sn.fixtures)
@@ -213,14 +218,24 @@ class MarkdownFile(pytest.File):
213
218
  sn_name=_sn_name,
214
219
  fpath=_fpath,
215
220
  fixture_names=_fixture_names,
221
+ is_pytestrun=_is_pytestrun,
216
222
  ):
217
223
  # This inner function *actually* has a **fixtures signature,
218
224
  # but we override __signature__ so pytest passes the right
219
225
  # fixtures and names.
220
226
  def test_block(**fixtures):
221
- # Auto-wrap async code
227
+ if is_pytestrun:
228
+ run_pytest_style_code(
229
+ code=code,
230
+ snippet_name=sn_name,
231
+ path=fpath,
232
+ )
233
+ return
234
+
235
+ # Normal (non-pytestrun) execution path
222
236
  ex_code = code
223
237
  if contains_top_level_await(code):
238
+ # Auto-wrap async code
224
239
  ex_code = wrap_async_code(code)
225
240
 
226
241
  try:
@@ -0,0 +1,63 @@
1
+ """
2
+ Helper module for running pytest-style tests found inside executed code blocks.
3
+ When a code block is marked with `pytestrun`, its code is written to a
4
+ temporary file and executed by pytest as a subprocess, so that fixtures,
5
+ markers, setup/teardown, and assertions all work correctly.
6
+ """
7
+ import os
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+
12
+ __author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
13
+ __copyright__ = "2025-2026 Artur Barseghyan"
14
+ __license__ = "MIT"
15
+ __all__ = ("run_pytest_style_code",)
16
+
17
+
18
+ def run_pytest_style_code(
19
+ code: str,
20
+ snippet_name: str,
21
+ path: str,
22
+ ) -> None:
23
+ """
24
+ Write the code block to a temporary file and run it with pytest.
25
+ Raises AssertionError on any test failures.
26
+ """
27
+ project_root = os.getcwd()
28
+ # Place the temp directory alongside the source file so that pytest walks
29
+ # up through its real directory hierarchy and discovers conftest.py files
30
+ # (including project-root ones that define fixtures).
31
+ source_dir = os.path.dirname(os.path.abspath(path))
32
+ pytest_cache_dir = os.path.join(source_dir, ".pytest_cache")
33
+ os.makedirs(pytest_cache_dir, exist_ok=True)
34
+ tmpdir = tempfile.mkdtemp(prefix="pytest_codeblock_", dir=pytest_cache_dir)
35
+ tmpfile = os.path.join(tmpdir, f"{snippet_name}.py")
36
+ try:
37
+ with open(tmpfile, "w") as f:
38
+ f.write(code)
39
+ env = os.environ.copy()
40
+ env["PYTHONPATH"] = os.pathsep.join(sys.path)
41
+ result = subprocess.run(
42
+ [
43
+ sys.executable, "-m", "pytest", tmpfile,
44
+ f"--rootdir={project_root}",
45
+ "--no-header", "-q",
46
+ ],
47
+ capture_output=True,
48
+ text=True,
49
+ cwd=project_root,
50
+ env=env,
51
+ )
52
+ if result.returncode != 0:
53
+ output = (result.stdout + result.stderr).strip()
54
+ raise AssertionError(
55
+ f"pytestrun block `{snippet_name}` in {path} failed:\n\n"
56
+ f"{output}"
57
+ )
58
+ finally:
59
+ try:
60
+ os.unlink(tmpfile)
61
+ os.rmdir(tmpdir)
62
+ except OSError:
63
+ pass
@@ -10,8 +10,14 @@ import pytest
10
10
 
11
11
  from .collector import CodeSnippet, group_snippets
12
12
  from .config import get_config
13
- from .constants import CODEBLOCK_MARK, DJANGO_DB_MARKS, TEST_PREFIX
13
+ from .constants import (
14
+ CODEBLOCK_MARK,
15
+ DJANGO_DB_MARKS,
16
+ PYTESTRUN_MARK,
17
+ TEST_PREFIX,
18
+ )
14
19
  from .helpers import contains_top_level_await, wrap_async_code
20
+ from .pytestrun import run_pytest_style_code
15
21
 
16
22
  __author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
17
23
  __copyright__ = "2025-2026 Artur Barseghyan"
@@ -129,10 +135,6 @@ def parse_rst(text: str, base_dir: Path) -> list[CodeSnippet]:
129
135
  )
130
136
  snippets.append(snippet)
131
137
 
132
- # TODO: Is this needed?
133
- # pending_marks.clear()
134
- # pending_fixtures.clear()
135
-
136
138
  i = j + 1
137
139
  continue
138
140
 
@@ -211,7 +213,7 @@ def parse_rst(text: str, base_dir: Path) -> list[CodeSnippet]:
211
213
  sn_marks = pending_marks.copy()
212
214
  sn_fixtures = pending_fixtures.copy()
213
215
  pending_name = None
214
- pending_marks = [CODEBLOCK_MARK] # clear pending marks
216
+ pending_marks = [CODEBLOCK_MARK] # clear pending marks
215
217
  pending_fixtures.clear()
216
218
 
217
219
  snippets.append(CodeSnippet(
@@ -241,7 +243,7 @@ def parse_rst(text: str, base_dir: Path) -> list[CodeSnippet]:
241
243
  sn_marks = pending_marks.copy()
242
244
  sn_fixtures = pending_fixtures.copy()
243
245
  pending_name = None
244
- pending_marks = [CODEBLOCK_MARK] # clear pending marks
246
+ pending_marks = [CODEBLOCK_MARK] # clear pending marks
245
247
  pending_fixtures.clear()
246
248
  j = i + 1
247
249
  if j < n and not lines[j].strip():
@@ -317,6 +319,7 @@ class RSTFile(pytest.File):
317
319
  # Bind the values we need so we don't close over `sn` itself
318
320
  _sn_name = sn.name
319
321
  _fpath = str(self.path)
322
+ _is_pytestrun = PYTESTRUN_MARK in sn.marks
320
323
 
321
324
  # Build list of fixture names requested by this snippet
322
325
  _fixture_names: list[str] = list(sn.fixtures)
@@ -334,11 +337,21 @@ class RSTFile(pytest.File):
334
337
  sn_name=_sn_name,
335
338
  fpath=_fpath,
336
339
  fixture_names=_fixture_names,
340
+ is_pytestrun=_is_pytestrun,
337
341
  ):
338
342
  # This inner function *actually* has a **fixtures signature,
339
343
  # but we override __signature__ so pytest passes the right
340
344
  # fixtures and names.
341
345
  def test_block(**fixtures):
346
+ if is_pytestrun:
347
+ run_pytest_style_code(
348
+ code=code,
349
+ snippet_name=sn_name,
350
+ path=fpath,
351
+ )
352
+ return
353
+
354
+ # Normal (non-pytestrun) execution path
342
355
  ex_code = code
343
356
  if contains_top_level_await(code):
344
357
  ex_code = wrap_async_code(code)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-codeblock
3
- Version: 0.5.2
3
+ Version: 0.5.4
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>
@@ -7,6 +7,7 @@ src/pytest_codeblock/config.py
7
7
  src/pytest_codeblock/constants.py
8
8
  src/pytest_codeblock/helpers.py
9
9
  src/pytest_codeblock/md.py
10
+ src/pytest_codeblock/pytestrun.py
10
11
  src/pytest_codeblock/rst.py
11
12
  src/pytest_codeblock.egg-info/PKG-INFO
12
13
  src/pytest_codeblock.egg-info/SOURCES.txt
@@ -18,4 +19,5 @@ src/pytest_codeblock/tests/__init__.py
18
19
  src/pytest_codeblock/tests/test_customisation.py
19
20
  src/pytest_codeblock/tests/test_integration.py
20
21
  src/pytest_codeblock/tests/test_nameless_codeblocks.py
21
- src/pytest_codeblock/tests/test_pytest_codeblock.py
22
+ src/pytest_codeblock/tests/test_pytest_codeblock.py
23
+ src/pytest_codeblock/tests/test_pytestrun_marker.py