pytest-codeblock 0.1__py3-none-any.whl

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.
@@ -0,0 +1,24 @@
1
+ from .md import MarkdownFile
2
+ from .rst import RSTFile
3
+
4
+ __title__ = "pytest-codeblock"
5
+ __version__ = "0.1"
6
+ __author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
7
+ __copyright__ = "2025 Artur Barseghyan"
8
+ __license__ = "MIT"
9
+ __all__ = (
10
+ "pytest_collect_file",
11
+ )
12
+
13
+
14
+ def pytest_collect_file(parent, path):
15
+ """Collect .md and .rst files for codeblock tests."""
16
+ # Determine file extension (works for py.path or pathlib.Path)
17
+ file_name = str(path).lower()
18
+ if file_name.endswith((".md", ".markdown")):
19
+ # Use the MarkdownFile collector for Markdown files
20
+ return MarkdownFile.from_parent(parent=parent, fspath=path)
21
+ if file_name.endswith(".rst"):
22
+ # Use the RSTFile collector for reStructuredText files
23
+ return RSTFile.from_parent(parent=parent, fspath=path)
24
+ return None
@@ -0,0 +1,68 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ import pytest
4
+
5
+ __author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
6
+ __copyright__ = "2025 Artur Barseghyan"
7
+ __license__ = "MIT"
8
+ __all__ = (
9
+ "CodeSnippet",
10
+ "CodeblockItem",
11
+ "group_snippets",
12
+ )
13
+
14
+
15
+ @dataclass
16
+ class CodeSnippet:
17
+ """Data container for an extracted code snippet."""
18
+ code: str # The code content
19
+ line: int # Starting line number in the source
20
+ name: str | None = None # Identifier for grouping (None if anonymous)
21
+ marks: list[str] = field(default_factory=list)
22
+ # Collected pytest marks (e.g. ['django_db']), 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
+ else:
46
+ sn.marks = list(sn.marks) # copy
47
+ seen[key] = sn
48
+ combined.append(sn)
49
+
50
+ return combined
51
+
52
+
53
+ class CodeblockItem(pytest.Item):
54
+ """A Pytest Item representing an executable code block."""
55
+ def __init__(self, *, name, parent, code: str, line: int):
56
+ super().__init__(name=name, parent=parent)
57
+ self.code = code
58
+ self.line = line
59
+
60
+ def runtest(self):
61
+ ns = {}
62
+ fname = str(self.fspath)
63
+ if self.name:
64
+ fname = f"{fname}::{self.name}"
65
+ exec(compile(self.code, fname, "exec"), ns)
66
+
67
+ def reportinfo(self):
68
+ return (self.fspath, self.line, f"code-block: {self.name}")
@@ -0,0 +1,15 @@
1
+ __author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
2
+ __copyright__ = "2025 Artur Barseghyan"
3
+ __license__ = "MIT"
4
+ __all__ = (
5
+ "DJANGO_DB_MARKS",
6
+ "TEST_PREFIX",
7
+ )
8
+
9
+ DJANGO_DB_MARKS = {
10
+ "django_db",
11
+ "db",
12
+ "transactional_db",
13
+ }
14
+
15
+ TEST_PREFIX = "test_"
pytest_codeblock/md.py ADDED
@@ -0,0 +1,161 @@
1
+ import re
2
+
3
+ import pytest
4
+
5
+ from .collector import CodeSnippet, group_snippets
6
+ from .constants import DJANGO_DB_MARKS, TEST_PREFIX
7
+
8
+ __author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
9
+ __copyright__ = "2025 Artur Barseghyan"
10
+ __license__ = "MIT"
11
+ __all__ = (
12
+ "MarkdownFile",
13
+ "parse_markdown",
14
+ )
15
+
16
+
17
+ def parse_markdown(text: str) -> list[CodeSnippet]:
18
+ """
19
+ Parse Markdown text and extract Python code snippets as CodeSnippet
20
+ objects.
21
+
22
+ Supports:
23
+ - <!-- pytestmark: <mark> --> comments immediately before a code fence
24
+ - <!-- codeblock-name: <name> --> comments for naming
25
+ - Fenced code blocks with ```python (and optional name=<name> in the
26
+ info string)
27
+
28
+ Captures each snippet’s name, code, starting line, and any pytest marks.
29
+ """
30
+ snippets: list[CodeSnippet] = []
31
+ lines = text.splitlines()
32
+ pending_name: str | None = None
33
+ pending_marks: list[str] = []
34
+ in_block = False
35
+ fence = ""
36
+ block_indent = 0
37
+ code_buffer: list[str] = []
38
+ snippet_name: str | None = None
39
+ start_line = 0
40
+
41
+ for idx, line in enumerate(lines, start=1):
42
+ stripped = line.strip()
43
+
44
+ if not in_block:
45
+ # Check for pytest mark comment
46
+ if stripped.startswith("<!--") and "pytestmark:" in stripped:
47
+ m = re.match(r"<!--\s*pytestmark:\s*(\w+)\s*-->", stripped)
48
+ if m:
49
+ pending_marks.append(m.group(1))
50
+ continue
51
+
52
+ # Check for name comment
53
+ if stripped.startswith("<!--") and "codeblock-name:" in stripped:
54
+ m = re.match(
55
+ r"<!--\s*codeblock-name:\s*([^ >]+)\s*-->", stripped
56
+ )
57
+ if m:
58
+ pending_name = m.group(1)
59
+ continue
60
+
61
+ # Start of fenced code block?
62
+ if line.lstrip().startswith("```"):
63
+ indent = len(line) - len(line.lstrip())
64
+ m = re.match(r"^`{3,}", line.lstrip())
65
+ if not m:
66
+ continue
67
+ fence = m.group(0)
68
+ info = line.lstrip()[len(fence):].strip()
69
+ parts = info.split(None, 1)
70
+ lang = parts[0].lower() if parts else ""
71
+ extra = parts[1] if len(parts) > 1 else ""
72
+ if lang in ("python", "py", "python3"):
73
+ in_block = True
74
+ block_indent = indent
75
+ start_line = idx + 1
76
+ code_buffer = []
77
+ # determine name from info string or pending comment
78
+ snippet_name = None
79
+ for token in extra.split():
80
+ if (
81
+ token.startswith("name=")
82
+ or token.startswith("name:")
83
+ ):
84
+ snippet_name = (
85
+ token.split("=", 1)[-1]
86
+ if "=" in token
87
+ else token.split(":", 1)[-1]
88
+ )
89
+ break
90
+ if snippet_name is None:
91
+ snippet_name = pending_name
92
+ # reset pending_name; marks stay until block closes
93
+ pending_name = None
94
+ continue
95
+
96
+ else:
97
+ # inside a fenced code block
98
+ if line.lstrip().startswith(fence):
99
+ # end of block
100
+ in_block = False
101
+ code_text = "\n".join(code_buffer)
102
+ snippets.append(CodeSnippet(
103
+ name=snippet_name,
104
+ code=code_text,
105
+ line=start_line,
106
+ marks=pending_marks.copy(),
107
+ ))
108
+ # reset pending marks after collecting
109
+ pending_marks.clear()
110
+ snippet_name = None
111
+ else:
112
+ # collect code lines (dedent by block_indent)
113
+ if line.strip() == "":
114
+ code_buffer.append("")
115
+ else:
116
+ if len(line) >= block_indent:
117
+ code_buffer.append(line[block_indent:])
118
+ else:
119
+ code_buffer.append(line.lstrip())
120
+ continue
121
+
122
+ return snippets
123
+
124
+
125
+ class MarkdownFile(pytest.File):
126
+ """
127
+ Collector for Markdown files, extracting only `test_`-prefixed code
128
+ snippets.
129
+ """
130
+ def collect(self):
131
+ text = self.fspath.read_text(encoding="utf-8")
132
+ raw = parse_markdown(text)
133
+ # keep only snippets named test_*
134
+ tests = [
135
+ sn for sn in raw if sn.name and sn.name.startswith(TEST_PREFIX)
136
+ ]
137
+ combined = group_snippets(tests)
138
+
139
+ for sn in combined:
140
+ # generate a real pytest Function so fixtures work
141
+ if DJANGO_DB_MARKS.intersection(sn.marks):
142
+ def make_func(code):
143
+ def test_block(db):
144
+ exec(code, {})
145
+ return test_block
146
+ else:
147
+ def make_func(code):
148
+ def test_block():
149
+ exec(code, {})
150
+ return test_block
151
+
152
+ callobj = make_func(sn.code)
153
+ fn = pytest.Function.from_parent(
154
+ parent=self,
155
+ name=sn.name,
156
+ callobj=callobj,
157
+ )
158
+ # apply any marks (e.g. django_db)
159
+ for m in sn.marks:
160
+ fn.add_marker(getattr(pytest.mark, m))
161
+ yield fn
@@ -0,0 +1,210 @@
1
+ import re
2
+
3
+ import pytest
4
+
5
+ from .collector import CodeSnippet, group_snippets
6
+ from .constants import DJANGO_DB_MARKS, TEST_PREFIX
7
+
8
+ __author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
9
+ __copyright__ = "2025 Artur Barseghyan"
10
+ __license__ = "MIT"
11
+ __all__ = (
12
+ "RSTFile",
13
+ "parse_rst",
14
+ )
15
+
16
+
17
+ def parse_rst(text: str) -> list[CodeSnippet]:
18
+ """
19
+ Parse an RST document into CodeSnippet objects, capturing:
20
+ - .. pytestmark: <mark>
21
+ - .. continue: <name>
22
+ - .. codeblock-name: <name>
23
+ - .. code-block:: python
24
+ """
25
+ snippets: list[CodeSnippet] = []
26
+ lines = text.splitlines()
27
+ n = len(lines)
28
+
29
+ pending_name: str | None = None
30
+ pending_marks: list[str] = []
31
+ pending_continue: str | None = None
32
+ i = 0
33
+
34
+ while i < n:
35
+ line = lines[i]
36
+
37
+ # Collect ".. pytestmark: xyz"
38
+ m = re.match(r"^\s*\.\.\s*pytestmark:\s*(\w+)\s*$", line)
39
+ if m:
40
+ pending_marks.append(m.group(1))
41
+ i += 1
42
+ continue
43
+
44
+ # Collect ".. continue: foo"
45
+ m = re.match(r"^\s*\.\.\s*continue:\s*(\S+)\s*$", line)
46
+ if m:
47
+ pending_continue = m.group(1)
48
+ i += 1
49
+ continue
50
+
51
+ # Collect ".. codeblock-name: foo"
52
+ m = re.match(r"^\s*\.\.\s*codeblock-name:\s*(\S+)\s*$", line)
53
+ if m:
54
+ pending_name = m.group(1)
55
+ i += 1
56
+ continue
57
+
58
+ # The code-block directive
59
+ m = re.match(r"^(\s*)\.\. (?:code-block|code)::\s*(\w+)", line)
60
+ if m:
61
+ base_indent = len(m.group(1))
62
+ lang = m.group(2).lower()
63
+ if lang in ("python", "py", "python3"):
64
+ # Parse :name: option
65
+ name_val: str | None = None
66
+ j = i + 1
67
+ while j < n:
68
+ ln = lines[j]
69
+ if not ln.strip():
70
+ j += 1
71
+ continue
72
+ indent = len(ln) - len(ln.lstrip())
73
+ if ln.lstrip().startswith(":") and indent > base_indent:
74
+ opt = ln.lstrip()
75
+ if opt.lower().startswith(":name:"):
76
+ name_val = opt.split(":", 2)[2].strip().split()[0]
77
+ j += 1
78
+ continue
79
+ break
80
+ # The j is first code line
81
+ if j >= n:
82
+ i = j
83
+ continue
84
+ first = lines[j]
85
+ content_indent = len(first) - len(first.lstrip())
86
+ if content_indent <= base_indent:
87
+ i = j
88
+ continue
89
+ # Collect code
90
+ buf: list[str] = []
91
+ k = j
92
+ while k < n:
93
+ ln = lines[k]
94
+ if not ln.strip():
95
+ buf.append("")
96
+ k += 1
97
+ continue
98
+ ind = len(ln) - len(ln.lstrip())
99
+ if ind >= content_indent:
100
+ buf.append(ln[content_indent:])
101
+ k += 1
102
+ else:
103
+ break
104
+ # Decide snippet name: continue overrides name_val/pending_name
105
+ if pending_continue:
106
+ sn_name = pending_continue
107
+ pending_continue = None
108
+ else:
109
+ sn_name = name_val or pending_name
110
+ sn_marks = pending_marks.copy()
111
+ pending_name = None
112
+ pending_marks.clear()
113
+
114
+ snippets.append(CodeSnippet(
115
+ name=sn_name,
116
+ code="\n".join(buf),
117
+ line=j + 1,
118
+ marks=sn_marks,
119
+ ))
120
+
121
+ i = k
122
+ continue
123
+ else:
124
+ i += 1
125
+ continue
126
+
127
+ # The literal-block via "::"
128
+ if line.rstrip().endswith("::") and pending_name:
129
+ # Similar override logic
130
+ if pending_continue:
131
+ sn_name = pending_continue
132
+ pending_continue = None
133
+ else:
134
+ sn_name = pending_name
135
+ sn_marks = pending_marks.copy()
136
+ pending_name = None
137
+ pending_marks.clear()
138
+ j = i + 1
139
+ if j < n and not lines[j].strip():
140
+ j += 1
141
+ if j >= n:
142
+ i = j
143
+ continue
144
+ first = lines[j]
145
+ content_indent = len(first) - len(first.lstrip())
146
+ buf: list[str] = []
147
+ k = j
148
+ while k < n:
149
+ ln = lines[k]
150
+ if not ln.strip():
151
+ buf.append("")
152
+ k += 1
153
+ continue
154
+ ind = len(ln) - len(ln.lstrip())
155
+ if ind >= content_indent:
156
+ buf.append(ln[content_indent:])
157
+ k += 1
158
+ else:
159
+ break
160
+ snippets.append(CodeSnippet(
161
+ name=sn_name,
162
+ code="\n".join(buf),
163
+ line=j + 1,
164
+ marks=sn_marks,
165
+ ))
166
+ i = k
167
+ continue
168
+
169
+ i += 1
170
+
171
+ return snippets
172
+
173
+
174
+ class RSTFile(pytest.File):
175
+ """Collect RST code-block tests as real test functions."""
176
+ def collect(self):
177
+ text = self.fspath.read_text(encoding="utf-8")
178
+ raw = parse_rst(text)
179
+
180
+ # Only keep test_* snippets
181
+ tests = [
182
+ sn for sn in raw if sn.name and sn.name.startswith(TEST_PREFIX)
183
+ ]
184
+ combined = group_snippets(tests)
185
+
186
+ for sn in combined:
187
+ # Create a Python function for this snippet
188
+ if DJANGO_DB_MARKS.intersection(sn.marks):
189
+ # Function *requests* the db fixture
190
+ def make_func(code):
191
+ def test_block(db):
192
+ exec(code, {})
193
+ return test_block
194
+ else:
195
+ def make_func(code):
196
+ def test_block():
197
+ exec(code, {})
198
+ return test_block
199
+
200
+ callobj = make_func(sn.code)
201
+
202
+ fn = pytest.Function.from_parent(
203
+ parent=self,
204
+ name=sn.name,
205
+ callobj=callobj
206
+ )
207
+ # Re-apply any pytest.mark.<foo> markers
208
+ for m in sn.marks:
209
+ fn.add_marker(getattr(pytest.mark, m))
210
+ yield fn
@@ -0,0 +1,323 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-codeblock
3
+ Version: 0.1
4
+ Summary: Pytest plugin to collect and test code blocks in reStructuredText and Markdown files.
5
+ Author-email: Artur Barseghyan <artur.barseghyan@gmail.com>
6
+ Maintainer-email: Artur Barseghyan <artur.barseghyan@gmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/barseghyanartur/pytest-codeblock/
9
+ Project-URL: Repository, https://github.com/barseghyanartur/pytest-codeblock/
10
+ Project-URL: Issues, https://github.com/barseghyanartur/pytest-codeblock/issues
11
+ Project-URL: Documentation, https://pytest-codeblock.readthedocs.io/
12
+ Project-URL: Changelog, https://pytest-codeblock.readthedocs.io/en/latest/changelog.html
13
+ Keywords: pytest,plugin,documentation,code blocks,markdown,rst
14
+ Classifier: Framework :: Pytest
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python
23
+ Classifier: Topic :: Software Development :: Testing
24
+ Classifier: Topic :: Software Development
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/x-rst
27
+ License-File: LICENSE
28
+ Requires-Dist: pytest
29
+ Provides-Extra: all
30
+ Requires-Dist: pytest-codeblock[build,dev,docs,test]; extra == "all"
31
+ Provides-Extra: dev
32
+ Requires-Dist: detect-secrets; extra == "dev"
33
+ Requires-Dist: doc8; extra == "dev"
34
+ Requires-Dist: ipython; extra == "dev"
35
+ Requires-Dist: mypy; extra == "dev"
36
+ Requires-Dist: pydoclint; extra == "dev"
37
+ Requires-Dist: ruff; extra == "dev"
38
+ Requires-Dist: twine; extra == "dev"
39
+ Requires-Dist: uv; extra == "dev"
40
+ Provides-Extra: test
41
+ Requires-Dist: django; extra == "test"
42
+ Requires-Dist: pytest; extra == "test"
43
+ Requires-Dist: pytest-cov; extra == "test"
44
+ Requires-Dist: pytest-django; extra == "test"
45
+ Provides-Extra: docs
46
+ Requires-Dist: sphinx<6.0; extra == "docs"
47
+ Requires-Dist: sphinx-autobuild; extra == "docs"
48
+ Requires-Dist: sphinx-rtd-theme>=1.3.0; extra == "docs"
49
+ Requires-Dist: sphinx-no-pragma; extra == "docs"
50
+ Provides-Extra: build
51
+ Requires-Dist: build; extra == "build"
52
+ Requires-Dist: twine; extra == "build"
53
+ Requires-Dist: wheel; extra == "build"
54
+ Dynamic: license-file
55
+
56
+ ================
57
+ pytest-codeblock
58
+ ================
59
+
60
+ .. External references
61
+ .. _reStructuredText: https://docutils.sourceforge.io/rst.html
62
+ .. _Markdown: https://daringfireball.net/projects/markdown/
63
+ .. _pytest: https://docs.pytest.org
64
+ .. _Django: https://www.djangoproject.com
65
+ .. _pip: https://pypi.org/project/pip/
66
+ .. _uv: https://pypi.org/project/uv/
67
+
68
+ .. Internal references
69
+
70
+ .. _pytest-codeblock: https://github.com/barseghyanartur/pytest-codeblock/
71
+ .. _Read the Docs: http://pytest-codeblock.readthedocs.io/
72
+ .. _Examples: https://github.com/barseghyanartur/pytest-codeblock/tree/main/examples
73
+ .. _Contributor guidelines: https://pytest-codeblock.readthedocs.io/en/latest/contributor_guidelines.html
74
+
75
+ Test your code blocks.
76
+
77
+ .. image:: https://img.shields.io/pypi/v/pytest-codeblock.svg
78
+ :target: https://pypi.python.org/pypi/pytest-codeblock
79
+ :alt: PyPI Version
80
+
81
+ .. image:: https://img.shields.io/pypi/pyversions/pytest-codeblock.svg
82
+ :target: https://pypi.python.org/pypi/pytest-codeblock/
83
+ :alt: Supported Python versions
84
+
85
+ .. image:: https://github.com/barseghyanartur/pytest-codeblock/actions/workflows/test.yml/badge.svg?branch=main
86
+ :target: https://github.com/barseghyanartur/pytest-codeblock/actions
87
+ :alt: Build Status
88
+
89
+ .. image:: https://readthedocs.org/projects/pytest-codeblock/badge/?version=latest
90
+ :target: http://pytest-codeblock.readthedocs.io
91
+ :alt: Documentation Status
92
+
93
+ .. image:: https://img.shields.io/badge/license-MIT-blue.svg
94
+ :target: https://github.com/barseghyanartur/pytest-codeblock/#License
95
+ :alt: MIT
96
+
97
+ .. image:: https://coveralls.io/repos/github/barseghyanartur/pytest-codeblock/badge.svg?branch=main&service=github
98
+ :target: https://coveralls.io/github/barseghyanartur/pytest-codeblock?branch=main
99
+ :alt: Coverage
100
+
101
+ `pytest-codeblock`_ is a `Pytest`_ plugin that discovers Python code examples
102
+ in your `reStructuredText`_ and `Markdown`_ documentation files and runs them
103
+ as part of your test suite. This ensures your docs stay correct and up-to-date.
104
+
105
+ Features
106
+ ========
107
+
108
+ - **Markdown and reST support**: Automatically finds fenced code blocks
109
+ in `.md`/`.markdown` files and `.. code-block:: python` or literal blocks
110
+ in `.rst` files.
111
+ - **Grouping by name**: Split a single example across multiple code blocks;
112
+ the plugin concatenates them into one test.
113
+ - **Minimal dependencies**: Only requires `pytest`_.
114
+
115
+ Prerequisites
116
+ =============
117
+ Python 3.10+
118
+
119
+ Documentation
120
+ =============
121
+ - Documentation is available on `Read the Docs`_.
122
+
123
+ Installation
124
+ ============
125
+
126
+ Install with `pip`_:
127
+
128
+ .. code-block:: sh
129
+
130
+ pip install pytest-codeblock
131
+
132
+ Or install with `uv`_:
133
+
134
+ .. code-block:: sh
135
+
136
+ uv pip install pytest-codeblock
137
+
138
+ Configuration
139
+ =============
140
+ *Filename: pyproject.toml*
141
+
142
+ .. code-block:: text
143
+
144
+ [tool.pytest.ini_options]
145
+ testpaths = [
146
+ "*.rst",
147
+ "**/*.rst",
148
+ "*.md",
149
+ "**/*.md",
150
+ ]
151
+
152
+ Usage
153
+ =====
154
+ reStructruredText usage
155
+ -----------------------
156
+ Any code directive, such as ``.. code-block:: python``, ``.. code:: python``,
157
+ or literal blocks with a preceding ``.. codeblock-name: <name>``, will be
158
+ collected and executed automatically, if your `pytest`_ `configuration`_
159
+ allows that.
160
+
161
+ **Basic example**
162
+
163
+ .. code-block:: rst
164
+
165
+ .. code-block:: python
166
+ :name: test_basic_example
167
+
168
+ import math
169
+
170
+ result = math.pow(3, 2)
171
+ assert result == 9
172
+
173
+ You can also use a literal block with a preceding name comment:
174
+
175
+ .. code-block:: rst
176
+
177
+ .. codeblock-name: test_grouping_example_literal_block
178
+ This is a literal block::
179
+
180
+ y = 5
181
+ print(y * 2)
182
+
183
+ **Grouping example**
184
+
185
+ It's possible to split one logical test into multiple blocks.
186
+ They will be tested under the first ``:name:`` specified.
187
+ Note the ``.. continue::`` directive.
188
+
189
+ .. code-block:: rst
190
+
191
+ .. code-block:: python
192
+ :name: test_grouping_example
193
+
194
+ x = 1
195
+
196
+ Some intervening text.
197
+
198
+ .. continue: test_grouping_example
199
+ .. code-block:: python
200
+ :name: test_grouping_example_part_2
201
+
202
+ y = x + 1 # Uses x from the first snippet
203
+ assert y == 2
204
+
205
+ Some intervening text.
206
+
207
+ .. continue: test_grouping_example
208
+ .. code-block:: python
209
+ :name: test_grouping_example_part_3
210
+
211
+ print(y) # Uses y from the previous snippet
212
+
213
+ The above mentioned three snippets will run as a single test.
214
+
215
+ **pytest marks**
216
+
217
+ .. code-block:: rst
218
+
219
+ .. pytestmark: django_db
220
+ .. code-block:: python
221
+ :name: test_django
222
+
223
+ from django.contrib.auth.models import User
224
+
225
+ user = User.objects.first()
226
+
227
+ Markdown usage
228
+ --------------
229
+
230
+ Any fenced code block with a recognized Python language tag (e.g., ``python``,
231
+ ``py``) will be collected and executed automatically, if your `pytest`_
232
+ `configuration`_ allows that.
233
+
234
+ **Basic example**
235
+
236
+ .. code-block:: markdown
237
+
238
+ ```python name=test_basic_example
239
+ import math
240
+
241
+ result = math.pow(3, 2)
242
+ assert result == 9
243
+ ```
244
+
245
+ **Grouping example**
246
+
247
+ .. code-block:: markdown
248
+
249
+ ```python name=test_grouping_example
250
+ x = 1
251
+ ```
252
+
253
+ Some intervening text.
254
+
255
+ ```python name=test_grouping_example
256
+ print(x + 1) # Uses x from the first snippet
257
+ ```
258
+
259
+ **pytest marks**
260
+
261
+ .. code-block:: markdown
262
+
263
+ <!-- pytestmark: django_db -->
264
+ ```python name=test_django
265
+ from django.contrib.auth.models import User
266
+
267
+ user = User.objects.first()
268
+ ```
269
+
270
+ Tests
271
+ =====
272
+
273
+ Run the tests with `pytest`_:
274
+
275
+ .. code-block:: sh
276
+
277
+ pytest
278
+
279
+ Writing documentation
280
+ =====================
281
+
282
+ Keep the following hierarchy.
283
+
284
+ .. code-block:: text
285
+
286
+ =====
287
+ title
288
+ =====
289
+
290
+ header
291
+ ======
292
+
293
+ sub-header
294
+ ----------
295
+
296
+ sub-sub-header
297
+ ~~~~~~~~~~~~~~
298
+
299
+ sub-sub-sub-header
300
+ ^^^^^^^^^^^^^^^^^^
301
+
302
+ sub-sub-sub-sub-header
303
+ ++++++++++++++++++++++
304
+
305
+ sub-sub-sub-sub-sub-header
306
+ **************************
307
+
308
+ License
309
+ =======
310
+
311
+ MIT
312
+
313
+ Support
314
+ =======
315
+ For security issues contact me at the e-mail given in the `Author`_ section.
316
+
317
+ For overall issues, go
318
+ to `GitHub <https://github.com/barseghyanartur/pytest-codeblock/issues>`_.
319
+
320
+ Author
321
+ ======
322
+
323
+ Artur Barseghyan <artur.barseghyan@gmail.com>
@@ -0,0 +1,11 @@
1
+ pytest_codeblock/__init__.py,sha256=lsy-UgNi1AxRkCZ9oY3Zn8yTDyNallRaXSoUrI1NmvA,822
2
+ pytest_codeblock/collector.py,sha256=zwCrjY0eGRK6695Gx4vEOcdv9ST-OYL15w7Ka2H_6vc,1939
3
+ pytest_codeblock/constants.py,sha256=ZlNBtQ85GGkB7DD2Te8AF2EeM4RQ6Bj0OUnPZAs9UhA,274
4
+ pytest_codeblock/md.py,sha256=WE73nfI5MrQawkwiXoFrY3gQ_p1uNWCX8fiDZqRHNV4,5612
5
+ pytest_codeblock/rst.py,sha256=eYtw0uJ17Xtj_-JkMpf9OKgK5ghgO0hYaIH649QKMBA,6564
6
+ pytest_codeblock-0.1.dist-info/licenses/LICENSE,sha256=dOWhRtenNzp0haV2LO9YRYb_-GCmPdwVJeTZ4MarauY,1073
7
+ pytest_codeblock-0.1.dist-info/METADATA,sha256=sbhcrBCeYBzF2_Ckv4zBNNTMoTPOUD2eTvUpchTFZXQ,8434
8
+ pytest_codeblock-0.1.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
9
+ pytest_codeblock-0.1.dist-info/entry_points.txt,sha256=jWxHxGWLzT4XZBSUMZ0aUnyDzOaDyjf-KnAIsYgObm0,47
10
+ pytest_codeblock-0.1.dist-info/top_level.txt,sha256=H2HyfKDFEj_5vWkbB6qITEjB-YgKfDxCPpIt_983K4g,17
11
+ pytest_codeblock-0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ pytest_codeblock = pytest_codeblock
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Artur Barseghyan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ pytest_codeblock