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.
- {pytest_codeblock-0.3.1/src/pytest_codeblock.egg-info → pytest_codeblock-0.3.2}/PKG-INFO +3 -2
- {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/README.rst +2 -1
- {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/pyproject.toml +1 -1
- {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock/__init__.py +2 -2
- pytest_codeblock-0.3.2/src/pytest_codeblock/helpers.py +38 -0
- {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock/md.py +20 -4
- {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock/rst.py +19 -4
- {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock/tests/test_pytest_codeblock.py +54 -1
- {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2/src/pytest_codeblock.egg-info}/PKG-INFO +3 -2
- {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock.egg-info/SOURCES.txt +1 -0
- {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/LICENSE +0 -0
- {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/setup.cfg +0 -0
- {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock/collector.py +0 -0
- {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock/constants.py +0 -0
- {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock/tests/__init__.py +0 -0
- {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock.egg-info/dependency_links.txt +0 -0
- {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock.egg-info/entry_points.txt +0 -0
- {pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
|
@@ -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.
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock.egg-info/requires.txt
RENAMED
|
File without changes
|
{pytest_codeblock-0.3.1 → pytest_codeblock-0.3.2}/src/pytest_codeblock.egg-info/top_level.txt
RENAMED
|
File without changes
|