pytest-codeblock 0.3.1__tar.gz → 0.3.2__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 (19) hide show
  1. {pytest_codeblock-0.3.1/src/pytest_codeblock.egg-info → pytest_codeblock-0.3.2}/PKG-INFO +3 -2
  2. {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/README.rst +2 -1
  3. {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/pyproject.toml +1 -1
  4. {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock/__init__.py +2 -2
  5. pytest_codeblock-0.3.2/src/pytest_codeblock/helpers.py +38 -0
  6. {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock/md.py +20 -4
  7. {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock/rst.py +19 -4
  8. {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock/tests/test_pytest_codeblock.py +54 -1
  9. {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2/src/pytest_codeblock.egg-info}/PKG-INFO +3 -2
  10. {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock.egg-info/SOURCES.txt +1 -0
  11. {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/LICENSE +0 -0
  12. {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/setup.cfg +0 -0
  13. {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock/collector.py +0 -0
  14. {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock/constants.py +0 -0
  15. {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock/tests/__init__.py +0 -0
  16. {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock.egg-info/dependency_links.txt +0 -0
  17. {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock.egg-info/entry_points.txt +0 -0
  18. {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock.egg-info/requires.txt +0 -0
  19. {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/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.3.1
3
+ Version: 0.3.2
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>
@@ -126,7 +126,8 @@ Features
126
126
  - **reStructuredText and Markdown support**: Automatically find and test code
127
127
  blocks in `reStructuredText`_ (``.rst``) and `Markdown`_ (``.md``) files.
128
128
  The only requirement here is that your code blocks shall
129
- have a name starting with ``test_``.
129
+ have a name starting with ``test_``. Async code snippets are supported as
130
+ well.
130
131
  - **Grouping by name**: Split a single example across multiple code blocks;
131
132
  the plugin concatenates them into one test.
132
133
  - **Pytest markers support**: Add existing or custom `pytest`_ markers
@@ -65,7 +65,8 @@ Features
65
65
  - **reStructuredText and Markdown support**: Automatically find and test code
66
66
  blocks in `reStructuredText`_ (``.rst``) and `Markdown`_ (``.md``) files.
67
67
  The only requirement here is that your code blocks shall
68
- have a name starting with ``test_``.
68
+ have a name starting with ``test_``. Async code snippets are supported as
69
+ well.
69
70
  - **Grouping by name**: Split a single example across multiple code blocks;
70
71
  the plugin concatenates them into one test.
71
72
  - **Pytest markers support**: Add existing or custom `pytest`_ markers
@@ -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.3.1"
5
+ version = "0.3.2"
6
6
  requires-python = ">=3.9"
7
7
  dependencies = [
8
8
  "pytest",
@@ -4,9 +4,9 @@ from .md import MarkdownFile
4
4
  from .rst import RSTFile
5
5
 
6
6
  __title__ = "pytest-codeblock"
7
- __version__ = "0.3.1"
7
+ __version__ = "0.3.2"
8
8
  __author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
9
- __copyright__ = "2025 Artur Barseghyan"
9
+ __copyright__ = "2025-2026 Artur Barseghyan"
10
10
  __license__ = "MIT"
11
11
  __all__ = (
12
12
  "pytest_collect_file",
@@ -0,0 +1,38 @@
1
+ import ast
2
+ import textwrap
3
+
4
+ __author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
5
+ __copyright__ = "2025-2026 Artur Barseghyan"
6
+ __license__ = "MIT"
7
+ __all__ = (
8
+ "contains_top_level_await",
9
+ "wrap_async_code",
10
+ )
11
+
12
+
13
+ def contains_top_level_await(code: str) -> bool:
14
+ """Analyzes code to detect presence of async patterns."""
15
+ try:
16
+ tree = ast.parse(code)
17
+ except SyntaxError:
18
+ # If the code is invalid, it technically doesn't
19
+ # contain valid async patterns.
20
+ return False
21
+
22
+ # Define the AST nodes that represent async constructs
23
+ async_nodes = (
24
+ ast.AsyncFunctionDef, # async def ...
25
+ ast.Await, # await ...
26
+ ast.AsyncWith, # async with ...
27
+ ast.AsyncFor, # async for ...
28
+ )
29
+
30
+ return any(isinstance(node, async_nodes) for node in ast.walk(tree))
31
+
32
+
33
+ def wrap_async_code(code: str) -> str:
34
+ """Wrap code containing top-level await in an async function."""
35
+ ind = textwrap.indent(code, " ")
36
+ return (
37
+ f"async def __async_main__():\n{ind}\n\nasyncio.run(__async_main__())"
38
+ )
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import inspect
2
3
  import re
3
4
  import textwrap
@@ -8,9 +9,10 @@ import pytest
8
9
 
9
10
  from .collector import CodeSnippet, group_snippets
10
11
  from .constants import CODEBLOCK_MARK, DJANGO_DB_MARKS, TEST_PREFIX
12
+ from .helpers import contains_top_level_await, wrap_async_code
11
13
 
12
14
  __author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
13
- __copyright__ = "2025 Artur Barseghyan"
15
+ __copyright__ = "2025-2026 Artur Barseghyan"
14
16
  __license__ = "MIT"
15
17
  __all__ = (
16
18
  "MarkdownFile",
@@ -176,16 +178,30 @@ class MarkdownFile(pytest.File):
176
178
  # but we override __signature__ so pytest passes the right
177
179
  # fixtures and names.
178
180
  def test_block(**fixtures):
179
- compiled = compile(code, fpath, "exec")
181
+ # Auto-wrap async code
182
+ ex_code = code
183
+ if contains_top_level_await(code):
184
+ ex_code = wrap_async_code(code)
185
+
186
+ try:
187
+ compiled = compile(ex_code, fpath, "exec")
188
+ except SyntaxError as err:
189
+ raise SyntaxError(
190
+ f"Syntax error in "
191
+ f"codeblock `{sn_name}` in {fpath}:\n"
192
+ f"\n{textwrap.indent(ex_code, prefix=' ')}\n\n"
193
+ f"{traceback.format_exc()}"
194
+ ) from err
195
+
180
196
  try:
181
197
  # Make fixtures available as top-level names
182
198
  # inside the executed snippet.
183
- exec(compiled, dict(fixtures))
199
+ exec(compiled, {"asyncio": asyncio, **dict(fixtures)})
184
200
  except Exception as err:
185
201
  raise Exception(
186
202
  f"Error in "
187
203
  f"codeblock `{sn_name}` in {fpath}:\n"
188
- f"\n{textwrap.indent(code, prefix=' ')}\n\n"
204
+ f"\n{textwrap.indent(ex_code, prefix=' ')}\n\n"
189
205
  f"{traceback.format_exc()}"
190
206
  ) from err
191
207
 
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import inspect
2
3
  import re
3
4
  import textwrap
@@ -9,9 +10,10 @@ import pytest
9
10
 
10
11
  from .collector import CodeSnippet, group_snippets
11
12
  from .constants import CODEBLOCK_MARK, DJANGO_DB_MARKS, TEST_PREFIX
13
+ from .helpers import contains_top_level_await, wrap_async_code
12
14
 
13
15
  __author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
14
- __copyright__ = "2025 Artur Barseghyan"
16
+ __copyright__ = "2025-2026 Artur Barseghyan"
15
17
  __license__ = "MIT"
16
18
  __all__ = (
17
19
  "RSTFile",
@@ -314,16 +316,29 @@ class RSTFile(pytest.File):
314
316
  # but we override __signature__ so pytest passes the right
315
317
  # fixtures and names.
316
318
  def test_block(**fixtures):
317
- compiled = compile(code, fpath, "exec")
319
+ ex_code = code
320
+ if contains_top_level_await(code):
321
+ ex_code = wrap_async_code(code)
322
+
323
+ try:
324
+ compiled = compile(ex_code, fpath, "exec")
325
+ except SyntaxError as err:
326
+ raise SyntaxError(
327
+ f"Syntax error in "
328
+ f"codeblock `{sn_name}` in {fpath}:\n"
329
+ f"\n{textwrap.indent(ex_code, prefix=' ')}\n\n"
330
+ f"{traceback.format_exc()}"
331
+ ) from err
332
+
318
333
  try:
319
334
  # Make fixtures available as top-level names
320
335
  # inside the executed snippet.
321
- exec(compiled, dict(fixtures))
336
+ exec(compiled, {"asyncio": asyncio, **dict(fixtures)})
322
337
  except Exception as err:
323
338
  raise Exception(
324
339
  f"Error in "
325
340
  f"codeblock `{sn_name}` in {fpath}:\n"
326
- f"\n{textwrap.indent(code, prefix=' ')}\n\n"
341
+ f"\n{textwrap.indent(ex_code, prefix=' ')}\n\n"
327
342
  f"{traceback.format_exc()}"
328
343
  ) from err
329
344
 
@@ -1,4 +1,5 @@
1
1
  from pytest_codeblock.collector import CodeSnippet, group_snippets
2
+ from pytest_codeblock.helpers import contains_top_level_await, wrap_async_code
2
3
  from pytest_codeblock.md import parse_markdown
3
4
  from pytest_codeblock.rst import (
4
5
  get_literalinclude_content,
@@ -7,7 +8,7 @@ from pytest_codeblock.rst import (
7
8
  )
8
9
 
9
10
  __author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
10
- __copyright__ = "2025 Artur Barseghyan"
11
+ __copyright__ = "2025-2026 Artur Barseghyan"
11
12
  __license__ = "MIT"
12
13
  __all__ = (
13
14
  "test_group_snippets_different_names",
@@ -119,3 +120,55 @@ def test_parse_rst_literalinclude(tmp_path):
119
120
  sn = snippets[0]
120
121
  assert sn.name == "test_li"
121
122
  assert "z=3" in sn.code
123
+
124
+
125
+ def test_contains_top_level_await_positive():
126
+ """Verify detection of various async constructs."""
127
+ # Direct await
128
+ assert contains_top_level_await("await asyncio.sleep(0)") is True
129
+ # Async function definition
130
+ assert contains_top_level_await("async def foo(): pass") is True
131
+ # Async with
132
+ assert contains_top_level_await("async with lock: pass") is True
133
+ # Async for
134
+ assert contains_top_level_await("async for i in range(1): pass") is True
135
+
136
+
137
+ def test_contains_top_level_await_negative():
138
+ """Verify that sync code or strings containing keywords are ignored."""
139
+ # Standard sync code
140
+ assert contains_top_level_await("import time; time.sleep(1)") is False
141
+ # Keywords inside strings
142
+ assert contains_top_level_await("print('this is an await')") is False
143
+ # Comments should be ignored
144
+ assert contains_top_level_await("# await inside comment") is False
145
+
146
+
147
+ def test_contains_top_level_await_invalid_syntax():
148
+ """Verify that invalid syntax returns False rather than crashing."""
149
+ assert contains_top_level_await("def main(:") is False
150
+
151
+
152
+ def test_wrap_async_code_structure():
153
+ """Verify the transformation logic and indentation."""
154
+ code = "await asyncio.sleep(1)\nreturn 42"
155
+ wrapped = wrap_async_code(code)
156
+
157
+ # Check for the boilerplate components
158
+ assert "async def __async_main__():" in wrapped
159
+ assert "asyncio.run(__async_main__())" in wrapped
160
+
161
+ # Check that the original code is indented correctly (4 spaces)
162
+ assert " await asyncio.sleep(1)" in wrapped
163
+ assert " return 42" in wrapped
164
+
165
+
166
+ def test_wrap_async_code_execution_integrity():
167
+ """
168
+ Verify that the wrapped code is still valid Python and can be compiled.
169
+ This ensures wrap_async_code doesn't break the AST.
170
+ """
171
+ code = "val = 1 + 1"
172
+ wrapped = wrap_async_code(code)
173
+ # If compile fails, the test fails
174
+ assert compile(wrapped, "<string>", "exec")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-codeblock
3
- Version: 0.3.1
3
+ Version: 0.3.2
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>
@@ -126,7 +126,8 @@ Features
126
126
  - **reStructuredText and Markdown support**: Automatically find and test code
127
127
  blocks in `reStructuredText`_ (``.rst``) and `Markdown`_ (``.md``) files.
128
128
  The only requirement here is that your code blocks shall
129
- have a name starting with ``test_``.
129
+ have a name starting with ``test_``. Async code snippets are supported as
130
+ well.
130
131
  - **Grouping by name**: Split a single example across multiple code blocks;
131
132
  the plugin concatenates them into one test.
132
133
  - **Pytest markers support**: Add existing or custom `pytest`_ markers
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  src/pytest_codeblock/__init__.py
5
5
  src/pytest_codeblock/collector.py
6
6
  src/pytest_codeblock/constants.py
7
+ src/pytest_codeblock/helpers.py
7
8
  src/pytest_codeblock/md.py
8
9
  src/pytest_codeblock/rst.py
9
10
  src/pytest_codeblock.egg-info/PKG-INFO