pytest-markdown-docs 0.9.1__tar.gz → 0.9.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 (21) hide show
  1. pytest_markdown_docs-0.9.1/README.md → pytest_markdown_docs-0.9.2/PKG-INFO +39 -0
  2. pytest_markdown_docs-0.9.1/PKG-INFO → pytest_markdown_docs-0.9.2/README.md +23 -13
  3. {pytest_markdown_docs-0.9.1 → pytest_markdown_docs-0.9.2}/pyproject.toml +16 -9
  4. {pytest_markdown_docs-0.9.1 → pytest_markdown_docs-0.9.2}/src/pytest_markdown_docs/_runners.py +18 -10
  5. {pytest_markdown_docs-0.9.1 → pytest_markdown_docs-0.9.2}/src/pytest_markdown_docs/plugin.py +47 -1
  6. pytest_markdown_docs-0.9.1/.github/pull_request_template.md +0 -5
  7. pytest_markdown_docs-0.9.1/.github/workflows/check.yml +0 -27
  8. pytest_markdown_docs-0.9.1/.github/workflows/ci.yml +0 -31
  9. pytest_markdown_docs-0.9.1/.github/workflows/codeql.yml +0 -74
  10. pytest_markdown_docs-0.9.1/.gitignore +0 -129
  11. pytest_markdown_docs-0.9.1/.pre-commit-config.yaml +0 -8
  12. pytest_markdown_docs-0.9.1/Makefile +0 -8
  13. pytest_markdown_docs-0.9.1/tests/conftest.py +0 -10
  14. pytest_markdown_docs-0.9.1/tests/plugin_test.py +0 -723
  15. pytest_markdown_docs-0.9.1/tests/support/docstring_error_after.py +0 -11
  16. pytest_markdown_docs-0.9.1/tests/support/docstring_error_before.py +0 -11
  17. {pytest_markdown_docs-0.9.1 → pytest_markdown_docs-0.9.2}/LICENSE +0 -0
  18. {pytest_markdown_docs-0.9.1 → pytest_markdown_docs-0.9.2}/src/pytest_markdown_docs/__init__.py +0 -0
  19. {pytest_markdown_docs-0.9.1 → pytest_markdown_docs-0.9.2}/src/pytest_markdown_docs/definitions.py +0 -0
  20. {pytest_markdown_docs-0.9.1 → pytest_markdown_docs-0.9.2}/src/pytest_markdown_docs/hooks.py +0 -0
  21. {pytest_markdown_docs-0.9.1 → pytest_markdown_docs-0.9.2}/src/pytest_markdown_docs/py.typed +0 -0
@@ -1,3 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-markdown-docs
3
+ Version: 0.9.2
4
+ Summary: Run markdown code fences through pytest
5
+ Author: Modal Labs, Elias Freider
6
+ Author-email: Elias Freider <elias@modal.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: markdown-it-py>=2.2.0,<4.0
10
+ Requires-Dist: pytest>=7.0.0
11
+ Requires-Python: >=3.9
12
+ Project-URL: Homepage, https://github.com/modal-labs/pytest-markdown-docs
13
+ Project-URL: Repository, https://github.com/modal-labs/pytest-markdown-docs
14
+ Project-URL: Issue Tracker, https://github.com/modal-labs/pytest-markdown-docs/issues
15
+ Description-Content-Type: text/markdown
16
+
1
17
  # Pytest Markdown Docs
2
18
 
3
19
  A plugin for [pytest](https://docs.pytest.org) that uses markdown code snippets from markdown files and docstrings as tests.
@@ -120,6 +136,29 @@ assert captured.out == "hello\n"
120
136
 
121
137
  As you can see above, the fixture value will be injected as a global. For `autouse=True` fixtures, the value is only injected as a global if it's explicitly added using a `fixture:<name>` marker.
122
138
 
139
+ ### Async Fixtures (pytest-asyncio)
140
+
141
+ If you have [pytest-asyncio](https://pypi.org/project/pytest-asyncio/) installed, you can use async fixtures with your markdown tests. The async fixture will be executed and its resolved value will be injected into the test code:
142
+
143
+ ```python
144
+ # conftest.py
145
+ import pytest_asyncio
146
+
147
+ @pytest_asyncio.fixture
148
+ async def async_data():
149
+ # Simulate async data fetching
150
+ return {"status": "ok", "value": 42}
151
+ ```
152
+
153
+ ````markdown
154
+ ```python fixture:async_data
155
+ assert async_data["status"] == "ok"
156
+ assert async_data["value"] == 42
157
+ ```
158
+ ````
159
+
160
+ Async fixtures can depend on other async fixtures, sync fixtures, or a mix of both. The fixture values are fully resolved before being passed to your markdown test code.
161
+
123
162
  ### Depending on previous snippets
124
163
 
125
164
  If you have multiple snippets following each other and want to keep the side
@@ -1,16 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: pytest-markdown-docs
3
- Version: 0.9.1
4
- Summary: Run markdown code fences through pytest
5
- Author: Modal Labs
6
- Author-email: Elias Freider <elias@modal.com>
7
- License-Expression: MIT
8
- License-File: LICENSE
9
- Requires-Python: >=3.9
10
- Requires-Dist: markdown-it-py<4.0,>=2.2.0
11
- Requires-Dist: pytest>=7.0.0
12
- Description-Content-Type: text/markdown
13
-
14
1
  # Pytest Markdown Docs
15
2
 
16
3
  A plugin for [pytest](https://docs.pytest.org) that uses markdown code snippets from markdown files and docstrings as tests.
@@ -133,6 +120,29 @@ assert captured.out == "hello\n"
133
120
 
134
121
  As you can see above, the fixture value will be injected as a global. For `autouse=True` fixtures, the value is only injected as a global if it's explicitly added using a `fixture:<name>` marker.
135
122
 
123
+ ### Async Fixtures (pytest-asyncio)
124
+
125
+ If you have [pytest-asyncio](https://pypi.org/project/pytest-asyncio/) installed, you can use async fixtures with your markdown tests. The async fixture will be executed and its resolved value will be injected into the test code:
126
+
127
+ ```python
128
+ # conftest.py
129
+ import pytest_asyncio
130
+
131
+ @pytest_asyncio.fixture
132
+ async def async_data():
133
+ # Simulate async data fetching
134
+ return {"status": "ok", "value": 42}
135
+ ```
136
+
137
+ ````markdown
138
+ ```python fixture:async_data
139
+ assert async_data["status"] == "ok"
140
+ assert async_data["value"] == 42
141
+ ```
142
+ ````
143
+
144
+ Async fixtures can depend on other async fixtures, sync fixtures, or a mix of both. The fixture values are fully resolved before being passed to your markdown test code.
145
+
136
146
  ### Depending on previous snippets
137
147
 
138
148
  If you have multiple snippets following each other and want to keep the side
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pytest-markdown-docs"
3
- version = "0.9.1"
3
+ version = "0.9.2"
4
4
  description = "Run markdown code fences through pytest"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -8,26 +8,33 @@ authors = [
8
8
  { name = "Elias Freider", email = "elias@modal.com" }
9
9
  ]
10
10
  license = "MIT"
11
+ license-files = ["LICENSE"]
11
12
  requires-python = ">=3.9"
12
13
  dependencies = [
13
14
  "markdown-it-py>=2.2.0,<4.0",
14
15
  "pytest>=7.0.0",
15
16
  ]
16
- include = ["LICENSE"]
17
+
18
+ [project.urls]
19
+ Homepage = "https://github.com/modal-labs/pytest-markdown-docs"
20
+ Repository = "https://github.com/modal-labs/pytest-markdown-docs"
21
+ "Issue Tracker" = "https://github.com/modal-labs/pytest-markdown-docs/issues"
22
+
17
23
 
18
24
  [project.entry-points.pytest11]
19
25
  pytest_markdown_docs = "pytest_markdown_docs.plugin"
20
26
 
21
27
  [build-system]
22
- requires = ["hatchling"]
23
- build-backend = "hatchling.build"
28
+ requires = ["uv_build>=0.10.9,<0.11.0"]
29
+ build-backend = "uv_build"
30
+
24
31
 
25
- [tool.uv]
26
- package=true
27
- dev-dependencies = [
32
+ [dependency-groups]
33
+ dev = [
28
34
  "mypy>=1.12.1",
29
35
  "pre-commit>=3.5.0",
30
- "pytest~=8.1.0",
36
+ "pytest>=8.1.0",
37
+ "pytest-asyncio>=0.23.0",
31
38
  "ruff~=0.9.10",
32
- "mdit-py-plugins~=0.4.2"
39
+ "mdit-py-plugins~=0.4.2",
33
40
  ]
@@ -1,5 +1,6 @@
1
1
  import abc
2
2
  import ast
3
+ import inspect
3
4
  import traceback
4
5
  import typing
5
6
  from abc import abstractmethod
@@ -50,22 +51,29 @@ def register_runner(*, default: bool = False):
50
51
 
51
52
  @register_runner(default=True)
52
53
  class DefaultRunner(_Runner):
53
- def runtest(self, test: FenceTestDefinition, args):
54
+ def runtest(self, test: FenceTestDefinition, args, *, asyncio_runner=None):
54
55
  try:
55
- tree = ast.parse(test.source, filename=test.source_path)
56
- except SyntaxError:
57
- raise
58
-
59
- try:
60
- # if we don't compile the code, it seems we get name lookup errors
61
- # for functions etc. when doing cross-calls across inline functions
62
56
  compiled = compile(
63
- tree, filename=test.source_path, mode="exec", dont_inherit=True
57
+ test.source,
58
+ filename=test.source_path,
59
+ mode="exec",
60
+ flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT,
61
+ dont_inherit=True,
64
62
  )
65
63
  except SyntaxError:
66
64
  raise
67
65
 
68
- exec(compiled, args)
66
+ if compiled.co_flags & inspect.CO_COROUTINE:
67
+ if asyncio_runner is None:
68
+ raise RuntimeError(
69
+ "Top-level async code in markdown code blocks is not natively supported.\n"
70
+ "You need pytest-asyncio>=1.1.0 to run async code blocks:\n"
71
+ " pip install 'pytest-asyncio>=1.1.0'"
72
+ )
73
+ coro = eval(compiled, args)
74
+ asyncio_runner.run(coro)
75
+ else:
76
+ exec(compiled, args)
69
77
 
70
78
  def repr_failure(
71
79
  self,
@@ -52,6 +52,14 @@ def get_docstring_start_line(obj) -> typing.Optional[int]:
52
52
  return None # Docstring not found in source
53
53
 
54
54
 
55
+ def _get_asyncio_runner(fixture_request):
56
+ """Try to fetch pytest-asyncio's event loop runner for shared-loop execution."""
57
+ try:
58
+ return fixture_request.getfixturevalue("_function_scoped_runner")
59
+ except Exception:
60
+ return None
61
+
62
+
55
63
  class MarkdownInlinePythonItem(pytest.Item):
56
64
  def __init__(
57
65
  self,
@@ -109,8 +117,17 @@ class MarkdownInlinePythonItem(pytest.Item):
109
117
  try:
110
118
  # this ensures that pytest's stdout/stderr capture works during the test:
111
119
  capman = self.config.pluginmanager.getplugin("capturemanager")
120
+ asyncio_runner = _get_asyncio_runner(self.fixture_request)
112
121
  with capman.global_and_fixture_disabled():
113
- self.runner.runtest(self.test_definition, all_globals)
122
+ try:
123
+ self.runner.runtest(
124
+ self.test_definition,
125
+ all_globals,
126
+ asyncio_runner=asyncio_runner,
127
+ )
128
+ except TypeError:
129
+ # Custom runner doesn't accept asyncio_runner kwarg
130
+ self.runner.runtest(self.test_definition, all_globals)
114
131
 
115
132
  # Success - test passed
116
133
  if attempt > 0:
@@ -282,8 +299,33 @@ def extract_options_from_mdx_comment(comment: str) -> typing.Set[str]:
282
299
  return set(option.strip() for option in comment.split(" ") if option)
283
300
 
284
301
 
302
+ def _preprocess_async_fixtures_if_available(collector: pytest.Collector) -> None:
303
+ """
304
+ If pytest-asyncio is installed, trigger its async fixture preprocessing.
305
+
306
+ pytest-asyncio preprocesses async fixtures during collection, but only for
307
+ pytest.Module and pytest.Class collectors. We need to trigger this manually
308
+ for our markdown collectors so that async fixtures work correctly.
309
+ """
310
+ try:
311
+ from pytest_asyncio.plugin import ( # type: ignore
312
+ _preprocess_async_fixtures,
313
+ _HOLDER,
314
+ )
315
+
316
+ _preprocess_async_fixtures(collector, _HOLDER)
317
+ except ImportError:
318
+ # pytest-asyncio is not installed, or using a newer version that
319
+ # doesn't require these imports
320
+ pass
321
+
322
+
285
323
  class MarkdownDocstringCodeModule(pytest.Module):
286
324
  def collect(self):
325
+ # Trigger pytest-asyncio's async fixture preprocessing if available
326
+ # (needed for pytest-asyncio 0.23.x; 1.x uses pytest_fixture_setup hook instead)
327
+ _preprocess_async_fixtures_if_available(self)
328
+
287
329
  if pytest.version_tuple >= (8, 1, 0):
288
330
  # consider_namespace_packages is a required keyword argument in pytest 8.1.0
289
331
  module = import_path(
@@ -361,6 +403,10 @@ class MarkdownDocstringCodeModule(pytest.Module):
361
403
 
362
404
  class MarkdownTextFile(pytest.File):
363
405
  def collect(self):
406
+ # Trigger pytest-asyncio's async fixture preprocessing if available
407
+ # (needed for pytest-asyncio 0.23.x; 1.x uses pytest_fixture_setup hook instead)
408
+ _preprocess_async_fixtures_if_available(self)
409
+
364
410
  markdown_content = self.path.read_text("utf8")
365
411
  fence_syntax = FenceSyntax(self.config.option.markdowndocs_syntax)
366
412
 
@@ -1,5 +0,0 @@
1
- <!--
2
- ✍️ Write a short summary of this change, then request review from a recent contributor.
3
- -->
4
-
5
- **Issue:** https://github.com/modal-labs/pytest-markdown-docs/issues/XX or N/A
@@ -1,27 +0,0 @@
1
- name: ruff & mypy
2
- on: push
3
-
4
- jobs:
5
- build:
6
- runs-on: ubuntu-latest
7
- steps:
8
- - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
9
- - name: Install uv
10
- uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
11
-
12
- - name: Install Python (3.11)
13
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
14
- with:
15
- python-version: 3.11
16
-
17
- - name: Install the project
18
- run: uv sync --all-extras --dev --no-install-project
19
-
20
- - name: Ruff check
21
- run: uv run ruff check --diff
22
-
23
- - name: Ruff format
24
- run: uv run ruff format --diff
25
-
26
- - name: Mypy
27
- run: uv run mypy .
@@ -1,31 +0,0 @@
1
- name: Test
2
- on: push
3
-
4
- jobs:
5
- build:
6
- runs-on: ubuntu-latest
7
- strategy:
8
- matrix:
9
- python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
10
- steps:
11
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
12
-
13
- - name: Install uv
14
- uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
15
-
16
- - name: Install Python ${{ matrix.python-version }}
17
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
18
- with:
19
- python-version: ${{ matrix.python-version }}
20
-
21
- - name: Install the project
22
- run: uv sync --all-extras --dev
23
-
24
- - name: Run tests with pytest
25
- run: uv run pytest
26
-
27
- - name: Downgrade to pytest 7
28
- run: uv pip install "pytest<8"
29
-
30
- - name: Run tests with pytest 7
31
- run: uv run pytest
@@ -1,74 +0,0 @@
1
- # For most projects, this workflow file will not need changing; you simply need
2
- # to commit it to your repository.
3
- #
4
- # You may wish to alter this file to override the set of languages analyzed,
5
- # or to provide custom queries or build logic.
6
- #
7
- # ******** NOTE ********
8
- # We have attempted to detect the languages in your repository. Please check
9
- # the `language` matrix defined below to confirm you have the correct set of
10
- # supported CodeQL languages.
11
- #
12
- name: "CodeQL"
13
-
14
- on:
15
- push:
16
- branches: [ "main" ]
17
- pull_request:
18
- schedule:
19
- - cron: '42 12 * * 0'
20
-
21
- jobs:
22
- analyze:
23
- name: Analyze
24
- runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
25
- permissions:
26
- actions: read
27
- contents: read
28
- security-events: write
29
-
30
- strategy:
31
- fail-fast: false
32
- matrix:
33
- language: [ 'python' ]
34
- # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
35
- # Use only 'java' to analyze code written in Java, Kotlin or both
36
- # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
37
- # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
38
-
39
- steps:
40
- - name: Checkout repository
41
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
42
-
43
- # Initializes the CodeQL tools for scanning.
44
- - name: Initialize CodeQL
45
- uses: github/codeql-action/init@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2
46
- with:
47
- languages: ${{ matrix.language }}
48
- # If you wish to specify custom queries, you can do so here or in a config file.
49
- # By default, queries listed here will override any specified in a config file.
50
- # Prefix the list here with "+" to use these queries and those in the config file.
51
-
52
- # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
53
- # queries: security-extended,security-and-quality
54
-
55
-
56
- # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
57
- # If this step fails, then you should remove it and run the build manually (see below)
58
- - name: Autobuild
59
- uses: github/codeql-action/autobuild@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2
60
-
61
- # ℹ️ Command-line programs to run using the OS shell.
62
- # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
63
-
64
- # If the Autobuild fails above, remove it and uncomment the following three lines.
65
- # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
66
-
67
- # - run: |
68
- # echo "Run, Build Application using script"
69
- # ./location_of_script_within_repo/buildscript.sh
70
-
71
- - name: Perform CodeQL Analysis
72
- uses: github/codeql-action/analyze@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2
73
- with:
74
- category: "/language:${{matrix.language}}"
@@ -1,129 +0,0 @@
1
- # Byte-compiled / optimized / DLL files
2
- __pycache__/
3
- *.py[cod]
4
- *$py.class
5
-
6
- # C extensions
7
- *.so
8
-
9
- # Distribution / packaging
10
- .Python
11
- build/
12
- develop-eggs/
13
- dist/
14
- downloads/
15
- eggs/
16
- .eggs/
17
- lib/
18
- lib64/
19
- parts/
20
- sdist/
21
- var/
22
- wheels/
23
- pip-wheel-metadata/
24
- share/python-wheels/
25
- *.egg-info/
26
- .installed.cfg
27
- *.egg
28
- MANIFEST
29
-
30
- # PyInstaller
31
- # Usually these files are written by a python script from a template
32
- # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
- *.manifest
34
- *.spec
35
-
36
- # Installer logs
37
- pip-log.txt
38
- pip-delete-this-directory.txt
39
-
40
- # Unit test / coverage reports
41
- htmlcov/
42
- .tox/
43
- .nox/
44
- .coverage
45
- .coverage.*
46
- .cache
47
- nosetests.xml
48
- coverage.xml
49
- *.cover
50
- *.py,cover
51
- .hypothesis/
52
- .pytest_cache/
53
-
54
- # Translations
55
- *.mo
56
- *.pot
57
-
58
- # Django stuff:
59
- *.log
60
- local_settings.py
61
- db.sqlite3
62
- db.sqlite3-journal
63
-
64
- # Flask stuff:
65
- instance/
66
- .webassets-cache
67
-
68
- # Scrapy stuff:
69
- .scrapy
70
-
71
- # Sphinx documentation
72
- docs/_build/
73
-
74
- # PyBuilder
75
- target/
76
-
77
- # Jupyter Notebook
78
- .ipynb_checkpoints
79
-
80
- # IPython
81
- profile_default/
82
- ipython_config.py
83
-
84
- # pyenv
85
- .python-version
86
-
87
- # pipenv
88
- # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
- # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
- # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
- # install all needed dependencies.
92
- #Pipfile.lock
93
-
94
- # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
- __pypackages__/
96
-
97
- # Celery stuff
98
- celerybeat-schedule
99
- celerybeat.pid
100
-
101
- # SageMath parsed files
102
- *.sage.py
103
-
104
- # Environments
105
- .env
106
- .venv
107
- env/
108
- venv/
109
- ENV/
110
- env.bak/
111
- venv.bak/
112
-
113
- # Spyder project settings
114
- .spyderproject
115
- .spyproject
116
-
117
- # Rope project settings
118
- .ropeproject
119
-
120
- # mkdocs documentation
121
- /site
122
-
123
- # mypy
124
- .mypy_cache/
125
- .dmypy.json
126
- dmypy.json
127
-
128
- # Pyre type checker
129
- .pyre/
@@ -1,8 +0,0 @@
1
- repos:
2
- - repo: https://github.com/charliermarsh/ruff-pre-commit
3
- rev: "v0.9.10"
4
- hooks:
5
- - id: ruff
6
- # Autofix, and respect `exclude` and `extend-exclude` settings.
7
- args: [--fix, --exit-non-zero-on-fix]
8
- - id: ruff-format
@@ -1,8 +0,0 @@
1
- build:
2
- uv build
3
-
4
- clean:
5
- rm -rf dist
6
-
7
- publish: clean build
8
- uv publish
@@ -1,10 +0,0 @@
1
- from pathlib import Path
2
-
3
- import pytest
4
-
5
- pytest_plugins = ["pytester"]
6
-
7
-
8
- @pytest.fixture()
9
- def support_dir():
10
- return Path(__file__).parent / "support"
@@ -1,723 +0,0 @@
1
- import re
2
-
3
- from _pytest.pytester import LineMatcher
4
-
5
- import pytest_markdown_docs # hack: used for storing a side effect in one of the tests
6
-
7
-
8
- def test_docstring_markdown(testdir):
9
- testdir.makeconftest(
10
- """
11
- def pytest_markdown_docs_globals():
12
- return {"a": "hello"}
13
- """
14
- )
15
- testdir.makepyfile(
16
- """
17
- def simple():
18
- \"\"\"
19
- ```python
20
- import pytest_markdown_docs
21
- pytest_markdown_docs.side_effect = "hello"
22
- ```
23
-
24
- ```
25
- not a python block
26
- ```
27
- \"\"\"
28
-
29
-
30
- class Parent:
31
- def using_global(self):
32
- \"\"\"
33
- ```python
34
- assert a + " world" == "hello world"
35
- ```
36
- \"\"\"
37
-
38
- def failing():
39
- \"\"\"
40
- ```python
41
- assert False
42
- ```
43
- \"\"\"
44
-
45
- def error():
46
- \"\"\"
47
- ```python
48
- raise Exception("oops")
49
- ```
50
- \"\"\"
51
- """
52
- )
53
- result = testdir.runpytest("--markdown-docs")
54
- result.assert_outcomes(passed=2, failed=2)
55
- assert (
56
- getattr(pytest_markdown_docs, "side_effect", None) == "hello"
57
- ) # hack to make sure the test actually does something
58
-
59
-
60
- def test_markdown_text_file(testdir):
61
- testdir.makeconftest(
62
- """
63
- def pytest_markdown_docs_globals():
64
- return {"a": "hello"}
65
- """
66
- )
67
-
68
- testdir.makefile(
69
- ".md",
70
- """
71
- ```python
72
- assert a + " world" == "hello world"
73
- ```
74
-
75
- ```python
76
- assert False
77
- ```
78
-
79
- ```python
80
- **@ # this is a syntax error
81
- ```
82
- """,
83
- )
84
- result = testdir.runpytest("--markdown-docs")
85
- result.assert_outcomes(passed=1, failed=2)
86
-
87
-
88
- def test_continuation(testdir):
89
- testdir.makefile(
90
- ".md",
91
- """
92
- ```python
93
- b = "hello"
94
- ```
95
-
96
- ```python continuation
97
- assert b + " world" == "hello world"
98
- ```
99
- """,
100
- )
101
- result = testdir.runpytest("--markdown-docs")
102
- result.assert_outcomes(passed=2)
103
-
104
-
105
- def test_traceback(testdir):
106
- testdir.makefile(
107
- ".md",
108
- """
109
- yada yada yada
110
-
111
- ```python
112
- def foo():
113
- raise Exception("doh")
114
-
115
- def bar():
116
- foo()
117
-
118
- foo()
119
- ```
120
- """,
121
- )
122
- result = testdir.runpytest("--markdown-docs")
123
- result.assert_outcomes(passed=0, failed=1)
124
-
125
- # we check the traceback vs a regex pattern since the file paths can change
126
- expected_output_pattern = r"""
127
- Error in code block:
128
- ```
129
- 4 def foo\(\):
130
- 5 raise Exception\("doh"\)
131
- 6
132
- 7 def bar\(\):
133
- 8 foo\(\)
134
- 9
135
- 10 foo\(\)
136
- ```
137
- Traceback \(most recent call last\):
138
- File ".*/test_traceback.md", line 10, in <module>
139
- foo\(\)
140
- File ".*/test_traceback.md", line 5, in foo
141
- raise Exception\("doh"\)
142
- Exception: doh
143
- """.strip()
144
- pytest_output = "\n".join(line.rstrip() for line in result.outlines).strip()
145
- assert re.search(expected_output_pattern, pytest_output) is not None, (
146
- "Output traceback doesn't match expected value"
147
- )
148
-
149
-
150
- def test_autouse_fixtures(testdir):
151
- testdir.makeconftest(
152
- """
153
- import pytest
154
-
155
- @pytest.fixture(autouse=True)
156
- def initialize():
157
- import pytest_markdown_docs
158
- pytest_markdown_docs.bump = getattr(pytest_markdown_docs, "bump", 0) + 1
159
- yield
160
- pytest_markdown_docs.bump -= 1
161
- """
162
- )
163
-
164
- testdir.makefile(
165
- ".md",
166
- """
167
- ```python
168
- import pytest_markdown_docs
169
- assert pytest_markdown_docs.bump == 1
170
- ```
171
- """,
172
- )
173
- result = testdir.runpytest("--markdown-docs")
174
- result.assert_outcomes(passed=1)
175
-
176
-
177
- def test_specific_fixtures(testdir):
178
- testdir.makeconftest(
179
- """
180
- import pytest
181
-
182
- @pytest.fixture()
183
- def initialize_specific():
184
- import pytest_markdown_docs
185
- pytest_markdown_docs.bump = getattr(pytest_markdown_docs, "bump", 0) + 1
186
- yield "foobar"
187
- pytest_markdown_docs.bump -= 1
188
- """
189
- )
190
-
191
- testdir.makefile(
192
- ".md",
193
- """
194
- \"\"\"
195
- ```python fixture:initialize_specific
196
- import pytest_markdown_docs
197
- assert pytest_markdown_docs.bump == 1
198
- assert initialize_specific == "foobar"
199
- ```
200
- \"\"\"
201
- """,
202
- )
203
- result = testdir.runpytest("--markdown-docs")
204
- result.assert_outcomes(passed=1)
205
-
206
-
207
- def test_non_existing_fixture_error(testdir):
208
- testdir.makeconftest(
209
- """
210
- import pytest
211
-
212
- @pytest.fixture()
213
- def foo():
214
- pass
215
- """
216
- )
217
-
218
- testdir.makefile(
219
- ".md",
220
- """
221
- \"\"\"
222
- ```python fixture:bar
223
- ```
224
- \"\"\"
225
- """,
226
- )
227
- result = testdir.runpytest("--markdown-docs")
228
- assert "fixture 'bar' not found" in result.stdout.str()
229
- result.assert_outcomes(errors=1)
230
-
231
-
232
- def test_fixture_overriding_global(testdir):
233
- testdir.makeconftest(
234
- """
235
- import pytest
236
-
237
- def pytest_markdown_docs_globals():
238
- return {
239
- "some_global": "foo"
240
- }
241
-
242
- @pytest.fixture()
243
- def some_global():
244
- return "bar"
245
- """
246
- )
247
-
248
- testdir.makefile(
249
- ".md",
250
- """
251
- \"\"\"
252
- ```python
253
- assert some_global == "foo"
254
- ```
255
-
256
- ```python fixture:some_global
257
- assert some_global == "bar"
258
- ```
259
- \"\"\"
260
- """,
261
- )
262
- result = testdir.runpytest("--markdown-docs")
263
- result.assert_outcomes(passed=2)
264
-
265
-
266
- def test_continuation_mdx_comment(testdir):
267
- testdir.makefile(
268
- ".mdx",
269
- """
270
- ```python
271
- b = "hello"
272
- ```
273
- {/* pmd-metadata: continuation */}
274
- ```python
275
- assert b + " world" == "hello world"
276
- ```
277
- """,
278
- )
279
- result = testdir.runpytest("--markdown-docs")
280
- result.assert_outcomes(passed=2)
281
-
282
-
283
- def test_specific_fixture_mdx_comment(testdir):
284
- testdir.makeconftest(
285
- """
286
- import pytest
287
- @pytest.fixture()
288
- def initialize_specific():
289
- import pytest_markdown_docs
290
- pytest_markdown_docs.bump = getattr(pytest_markdown_docs, "bump", 0) + 1
291
- yield "foobar"
292
- pytest_markdown_docs.bump -= 1
293
- """
294
- )
295
-
296
- testdir.makefile(
297
- ".mdx",
298
- """
299
- {/* pmd-metadata: fixture:initialize_specific */}
300
- ```python
301
- import pytest_markdown_docs
302
- assert pytest_markdown_docs.bump == 1
303
- assert initialize_specific == "foobar"
304
- ```
305
- """,
306
- )
307
- result = testdir.runpytest("--markdown-docs")
308
- result.assert_outcomes(passed=1)
309
-
310
-
311
- def test_multiple_fixtures_mdx_comment(testdir):
312
- testdir.makeconftest(
313
- """
314
- import pytest
315
- @pytest.fixture()
316
- def initialize_specific():
317
- import pytest_markdown_docs
318
- pytest_markdown_docs.bump = getattr(pytest_markdown_docs, "bump", 0) + 1
319
- yield "foobar"
320
- pytest_markdown_docs.bump -= 1
321
-
322
- @pytest.fixture
323
- def another_fixture():
324
- return "hello"
325
- """
326
- )
327
-
328
- testdir.makefile(
329
- ".mdx",
330
- """
331
- {/* pmd-metadata: fixture:initialize_specific fixture:another_fixture */}
332
- ```python
333
- import pytest_markdown_docs
334
- assert pytest_markdown_docs.bump == 1
335
- assert initialize_specific == "foobar"
336
- ```
337
- """,
338
- )
339
- result = testdir.runpytest("--markdown-docs")
340
- result.assert_outcomes(passed=1)
341
-
342
-
343
- def test_notest_mdx_comment(testdir):
344
- testdir.makefile(
345
- ".mdx",
346
- """
347
- {/* pmd-metadata: notest */}
348
- ```python
349
- assert True
350
- ```
351
- """,
352
- )
353
- result = testdir.runpytest("--markdown-docs")
354
- result.assert_outcomes(passed=0)
355
-
356
-
357
- def test_superfences_format_markdown(testdir):
358
- testdir.makefile(
359
- ".md",
360
- """
361
- ```python
362
- b = "hello"
363
- ```
364
-
365
- ```{.python continuation}
366
- assert b + " world" == "hello world"
367
- ```
368
-
369
- # the lang may not be the first element
370
- ```{other_option .python .other-class continuation}
371
- assert b + " world" == "hello world"
372
- ```
373
- """,
374
- )
375
- result = testdir.runpytest("--markdown-docs", "--markdown-docs-syntax=superfences")
376
- result.assert_outcomes(passed=3)
377
-
378
-
379
- def test_superfences_format_docstring(testdir):
380
- testdir.makepyfile(
381
- """
382
- def simple():
383
- \"\"\"
384
- ```python
385
- b = "hello"
386
- ```
387
-
388
- ```{.python continuation}
389
- assert b + " world" == "hello world"
390
- ```
391
- \"\"\"
392
- """
393
- )
394
- result = testdir.runpytest("--markdown-docs", "--markdown-docs-syntax=superfences")
395
- result.assert_outcomes(passed=2)
396
-
397
-
398
- def test_error_origin_after_docstring_traceback(testdir, support_dir):
399
- sample_file = support_dir / "docstring_error_after.py"
400
- testdir.makepyfile(**{sample_file.stem: sample_file.read_text()})
401
- result = testdir.runpytest("-v", "--markdown-docs")
402
-
403
- data: LineMatcher = result.stdout
404
- data.re_match_lines(
405
- [
406
- r"Traceback \(most recent call last\):",
407
- r'\s*File ".*/docstring_error_after.py", line 5, in <module>',
408
- r"\s*docstring_error_after.error_after\(\)",
409
- r'\s*File ".*/docstring_error_after.py", line 11, in error_after',
410
- r'\s*raise Exception\("bar"\)',
411
- r"\s*Exception: bar",
412
- ],
413
- consecutive=True,
414
- )
415
-
416
-
417
- def test_error_origin_before_docstring_traceback(testdir, support_dir):
418
- sample_file = support_dir / "docstring_error_before.py"
419
- testdir.makepyfile(**{sample_file.stem: sample_file.read_text()})
420
- result = testdir.runpytest("-v", "--markdown-docs")
421
-
422
- data: LineMatcher = result.stdout
423
- data.re_match_lines(
424
- [
425
- r"Traceback \(most recent call last\):",
426
- r'\s*File ".*/docstring_error_before.py", line 9, in <module>',
427
- r"\s*docstring_error_before.error_before\(\)",
428
- r'\s*File ".*/docstring_error_before.py", line 2, in error_before',
429
- r'\s*raise Exception\("foo"\)',
430
- r"\s*Exception: foo",
431
- ],
432
- consecutive=True,
433
- )
434
-
435
-
436
- def test_custom_runner(testdir):
437
- testdir.makeconftest(
438
- """
439
- import pytest_markdown_docs._runners
440
-
441
- @pytest_markdown_docs._runners.register_runner()
442
- class LinesAreAllFoo(pytest_markdown_docs._runners.DefaultRunner):
443
- def runtest(self, test, args):
444
- lines = test.source.strip().split("\\n")
445
- for line in lines:
446
- assert line == "foo"
447
- """
448
- )
449
- testdir.makefile(
450
- ".md",
451
- """
452
- ```python runner:LinesAreAllFoo
453
- foo
454
- foo
455
- ```
456
-
457
- ```python runner:LinesAreAllFoo
458
- foo
459
- bar
460
- ```
461
- """,
462
- )
463
-
464
- result = testdir.runpytest("-v", "--markdown-docs")
465
- result.assert_outcomes(passed=1, failed=1)
466
- result.stdout.re_match_lines(
467
- [
468
- r".*\[CodeFence#1\]\[line:1\].*PASSED.*",
469
- r".*\[CodeFence#2\]\[line:6\].*FAILED.*",
470
- ],
471
- consecutive=True,
472
- )
473
-
474
-
475
- def test_admonition_markdown_text_file(testdir):
476
- testdir.makeconftest(
477
- """
478
- def pytest_markdown_docs_globals():
479
- return {"a": "hello"}
480
-
481
- def pytest_markdown_docs_markdown_it():
482
- import markdown_it
483
- from mdit_py_plugins.admon import admon_plugin
484
-
485
- mi = markdown_it.MarkdownIt(config="commonmark")
486
- mi.use(admon_plugin)
487
- return mi
488
- """
489
- )
490
-
491
- testdir.makefile(
492
- ".md",
493
- """
494
- ??? quote
495
-
496
- ```python
497
- assert a + " world" == "hello world"
498
- ```
499
-
500
- !!! info
501
- ```python
502
- assert False
503
- ```
504
-
505
- ???+ note
506
- ```python
507
- **@ # this is a syntax error
508
- ```
509
- """,
510
- )
511
- result = testdir.runpytest("--markdown-docs")
512
- result.assert_outcomes(passed=1, failed=2)
513
-
514
-
515
- def test_retry_eventually_succeeds(testdir):
516
- """Test that a flaky test succeeds after retrying."""
517
- testdir.makepyfile(
518
- conftest="""
519
- attempt_counter = {}
520
- """
521
- )
522
- testdir.makefile(
523
- ".md",
524
- test_file="""
525
- ```python retry:3
526
- import conftest
527
- key = "test_1"
528
- conftest.attempt_counter[key] = conftest.attempt_counter.get(key, 0) + 1
529
- assert conftest.attempt_counter[key] >= 2 # Fails first time, passes on retry
530
- ```
531
- """,
532
- )
533
- result = testdir.runpytest("--markdown-docs")
534
- result.assert_outcomes(passed=1)
535
-
536
-
537
- def test_retry_exhausted(testdir):
538
- """Test that a test fails after all retry attempts are exhausted."""
539
- testdir.makefile(
540
- ".md",
541
- test_file="""
542
- ```python retry:2
543
- assert False # Always fails
544
- ```
545
- """,
546
- )
547
- result = testdir.runpytest("--markdown-docs")
548
- result.assert_outcomes(failed=1)
549
-
550
-
551
- def test_retry_with_fixture(testdir):
552
- """Test that fixtures are not re-run between retries."""
553
- testdir.makepyfile(
554
- conftest="""
555
- import pytest
556
-
557
- fixture_call_count = 0
558
-
559
- @pytest.fixture
560
- def counting_fixture():
561
- global fixture_call_count
562
- fixture_call_count += 1
563
- return fixture_call_count
564
-
565
- attempt_counter = {}
566
- """
567
- )
568
- testdir.makefile(
569
- ".md",
570
- test_file="""
571
- ```python retry:3 fixture:counting_fixture
572
- import conftest
573
- key = "test_2"
574
- conftest.attempt_counter[key] = conftest.attempt_counter.get(key, 0) + 1
575
-
576
- # Fixture should only be called once (value should stay 1)
577
- assert counting_fixture == 1
578
-
579
- # Fail on first attempt, pass on second
580
- assert conftest.attempt_counter[key] >= 2
581
- ```
582
- """,
583
- )
584
- result = testdir.runpytest("--markdown-docs")
585
- result.assert_outcomes(passed=1)
586
-
587
-
588
- def test_retry_with_continuation(testdir):
589
- """Test that retry works with continuation blocks."""
590
- testdir.makepyfile(
591
- conftest="""
592
- attempt_counter = {}
593
- """
594
- )
595
- testdir.makefile(
596
- ".md",
597
- test_file="""
598
- ```python
599
- a = "hello"
600
- ```
601
-
602
- ```python retry:2 continuation
603
- import conftest
604
- key = "test_3"
605
- conftest.attempt_counter[key] = conftest.attempt_counter.get(key, 0) + 1
606
-
607
- # Variable 'a' from previous block should be available
608
- assert a + " world" == "hello world"
609
-
610
- # Fail on first attempt, pass on second
611
- assert conftest.attempt_counter[key] >= 2
612
- ```
613
- """,
614
- )
615
- result = testdir.runpytest("--markdown-docs")
616
- result.assert_outcomes(passed=2)
617
-
618
-
619
- def test_retry_invalid_negative(testdir):
620
- """Test that negative retry counts raise an error."""
621
- testdir.makefile(
622
- ".md",
623
- test_file="""
624
- ```python retry:-1
625
- assert True
626
- ```
627
- """,
628
- )
629
- result = testdir.runpytest("--markdown-docs")
630
- result.assert_outcomes(errors=1)
631
- result.stdout.fnmatch_lines(["*Invalid retry count*non-negative integer*"])
632
-
633
-
634
- def test_retry_invalid_non_numeric(testdir):
635
- """Test that non-numeric retry counts raise an error."""
636
- testdir.makefile(
637
- ".md",
638
- test_file="""
639
- ```python retry:abc
640
- assert True
641
- ```
642
- """,
643
- )
644
- result = testdir.runpytest("--markdown-docs")
645
- result.assert_outcomes(errors=1)
646
- result.stdout.fnmatch_lines(["*Invalid retry count*non-negative integer*"])
647
-
648
-
649
- def test_retry_multiple_values_error(testdir):
650
- """Test that multiple retry values raise an error."""
651
- testdir.makefile(
652
- ".md",
653
- test_file="""
654
- ```python retry:2 retry:3
655
- assert True
656
- ```
657
- """,
658
- )
659
- result = testdir.runpytest("--markdown-docs")
660
- result.assert_outcomes(errors=1)
661
- result.stdout.fnmatch_lines(["*Multiple retry counts are not supported*"])
662
-
663
-
664
- def test_retry_zero(testdir):
665
- """Test that retry:0 behaves the same as no retry."""
666
- testdir.makefile(
667
- ".md",
668
- test_file="""
669
- ```python retry:0
670
- assert False
671
- ```
672
- """,
673
- )
674
- result = testdir.runpytest("--markdown-docs")
675
- result.assert_outcomes(failed=1)
676
-
677
-
678
- def test_retry_mdx_comment(testdir):
679
- """Test that retry works with MDX comment metadata."""
680
- testdir.makepyfile(
681
- conftest="""
682
- attempt_counter = {}
683
- """
684
- )
685
- testdir.makefile(
686
- ".mdx",
687
- test_file="""
688
- {/* pmd-metadata: retry:3 */}
689
- ```python
690
- import conftest
691
- key = "test_4"
692
- conftest.attempt_counter[key] = conftest.attempt_counter.get(key, 0) + 1
693
- assert conftest.attempt_counter[key] >= 2
694
- ```
695
- """,
696
- )
697
- result = testdir.runpytest("--markdown-docs")
698
- result.assert_outcomes(passed=1)
699
-
700
-
701
- def test_retry_with_docstring(testdir):
702
- """Test that retry works in Python docstrings."""
703
- testdir.makepyfile(
704
- conftest="""
705
- attempt_counter = {}
706
- """
707
- )
708
- testdir.makepyfile(
709
- test_module="""
710
- def my_function():
711
- \"\"\"
712
- ```python retry:3
713
- import conftest
714
- key = "test_5"
715
- conftest.attempt_counter[key] = conftest.attempt_counter.get(key, 0) + 1
716
- assert conftest.attempt_counter[key] >= 2
717
- ```
718
- \"\"\"
719
- pass
720
- """
721
- )
722
- result = testdir.runpytest("--markdown-docs")
723
- result.assert_outcomes(passed=1)
@@ -1,11 +0,0 @@
1
- def func():
2
- """
3
- ```python
4
- import docstring_error_after
5
- docstring_error_after.error_after()
6
- ```
7
- """
8
-
9
-
10
- def error_after():
11
- raise Exception("bar")
@@ -1,11 +0,0 @@
1
- def error_before():
2
- raise Exception("foo")
3
-
4
-
5
- def func():
6
- """
7
- ```python
8
- import docstring_error_before
9
- docstring_error_before.error_before()
10
- ```
11
- """