pytest-markdown-docs 0.6.0__tar.gz → 0.7.0__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 (24) hide show
  1. pytest_markdown_docs-0.7.0/Makefile +8 -0
  2. pytest_markdown_docs-0.6.0/README.md → pytest_markdown_docs-0.7.0/PKG-INFO +27 -1
  3. pytest_markdown_docs-0.6.0/PKG-INFO → pytest_markdown_docs-0.7.0/README.md +15 -14
  4. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.0}/pyproject.toml +1 -3
  5. pytest_markdown_docs-0.7.0/pytest-markdown-docs.iml +13 -0
  6. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.0}/src/pytest_markdown_docs/plugin.py +158 -83
  7. pytest_markdown_docs-0.7.0/tests/conftest.py +10 -0
  8. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.0}/tests/plugin_test.py +84 -3
  9. pytest_markdown_docs-0.7.0/tests/support/docstring_error_after.py +11 -0
  10. pytest_markdown_docs-0.7.0/tests/support/docstring_error_before.py +11 -0
  11. pytest_markdown_docs-0.6.0/poetry.lock +0 -410
  12. pytest_markdown_docs-0.6.0/poetry.toml +0 -3
  13. pytest_markdown_docs-0.6.0/tests/conftest.py +0 -1
  14. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.0}/.github/pull_request_template.md +0 -0
  15. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.0}/.github/workflows/check.yml +0 -0
  16. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.0}/.github/workflows/ci.yml +0 -0
  17. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.0}/.github/workflows/codeql.yml +0 -0
  18. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.0}/.gitignore +0 -0
  19. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.0}/.pre-commit-config.yaml +0 -0
  20. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.0}/LICENSE +0 -0
  21. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.0}/src/pytest_markdown_docs/__init__.py +0 -0
  22. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.0}/src/pytest_markdown_docs/hooks.py +0 -0
  23. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.0}/src/pytest_markdown_docs/py.typed +0 -0
  24. {pytest_markdown_docs-0.6.0 → pytest_markdown_docs-0.7.0}/uv.lock +0 -0
@@ -0,0 +1,8 @@
1
+ build:
2
+ uv build
3
+
4
+ clean:
5
+ rm -rf dist
6
+
7
+ publish: clean build
8
+ uv publish
@@ -1,3 +1,15 @@
1
+ Metadata-Version: 2.3
2
+ Name: pytest-markdown-docs
3
+ Version: 0.7.0
4
+ Summary: Run markdown code fences through pytest
5
+ Author: Modal Labs
6
+ Author-email: Elias Freider <elias@modal.com>
7
+ License: MIT
8
+ Requires-Python: >=3.8
9
+ Requires-Dist: markdown-it-py<4.0,>=2.2.0
10
+ Requires-Dist: pytest>=7.0.0
11
+ Description-Content-Type: text/markdown
12
+
1
13
  # Pytest Markdown Docs
2
14
 
3
15
  A plugin for [pytest](https://docs.pytest.org) that uses markdown code snippets from markdown files and docstrings as tests.
@@ -136,6 +148,20 @@ assert a + " world" == "hello world"
136
148
  ```
137
149
  ````
138
150
 
151
+ ### Compatibility with Material for MkDocs
152
+
153
+ Material for Mkdocs is not compatible with the default syntax.
154
+
155
+ But if the extension `pymdownx.superfences` is configured for mkdocs, the brace format can be used:
156
+ ````markdown
157
+ ```{.python continuation}
158
+ ````
159
+
160
+ You will need to call pytest with the `--markdown-docs-syntax` option:
161
+ ```shell
162
+ pytest --markdown-docs --markdown-docs-syntax=superfences
163
+ ```
164
+
139
165
  ## MDX Comments for Metadata Options
140
166
  In .mdx files, you can use MDX comments to provide additional options for code blocks. These comments should be placed immediately before the code block and take the following form:
141
167
 
@@ -175,4 +201,4 @@ Or for fun, you can use this plugin to include testing of the validity of snippe
175
201
  * Line numbers are "wrong" for docstring-inlined snippets (since we don't know where in the file the docstring starts)
176
202
  * Line numbers are "wrong" for continuation blocks even in pure markdown files (can be worked out with some refactoring)
177
203
  * There are probably more appropriate ways to use pytest internal APIs to get more features "for free" - current state of the code is a bit "patch it til' it works".
178
- * Assertions are not rewritten w/ pretty data structure inspection like they are with regular pytest tests by default
204
+ * Assertions are not rewritten w/ pretty data structure inspection like they are with regular pytest tests by default
@@ -1,16 +1,3 @@
1
- Metadata-Version: 2.3
2
- Name: pytest-markdown-docs
3
- Version: 0.6.0
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.8
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.
@@ -149,6 +136,20 @@ assert a + " world" == "hello world"
149
136
  ```
150
137
  ````
151
138
 
139
+ ### Compatibility with Material for MkDocs
140
+
141
+ Material for Mkdocs is not compatible with the default syntax.
142
+
143
+ But if the extension `pymdownx.superfences` is configured for mkdocs, the brace format can be used:
144
+ ````markdown
145
+ ```{.python continuation}
146
+ ````
147
+
148
+ You will need to call pytest with the `--markdown-docs-syntax` option:
149
+ ```shell
150
+ pytest --markdown-docs --markdown-docs-syntax=superfences
151
+ ```
152
+
152
153
  ## MDX Comments for Metadata Options
153
154
  In .mdx files, you can use MDX comments to provide additional options for code blocks. These comments should be placed immediately before the code block and take the following form:
154
155
 
@@ -188,4 +189,4 @@ Or for fun, you can use this plugin to include testing of the validity of snippe
188
189
  * Line numbers are "wrong" for docstring-inlined snippets (since we don't know where in the file the docstring starts)
189
190
  * Line numbers are "wrong" for continuation blocks even in pure markdown files (can be worked out with some refactoring)
190
191
  * There are probably more appropriate ways to use pytest internal APIs to get more features "for free" - current state of the code is a bit "patch it til' it works".
191
- * Assertions are not rewritten w/ pretty data structure inspection like they are with regular pytest tests by default
192
+ * Assertions are not rewritten w/ pretty data structure inspection like they are with regular pytest tests by default
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pytest-markdown-docs"
3
- version = "0.6.0"
3
+ version = "0.7.0"
4
4
  description = "Run markdown code fences through pytest"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -30,5 +30,3 @@ dev-dependencies = [
30
30
  "pytest~=8.1.0",
31
31
  "ruff>=0.7.0",
32
32
  ]
33
-
34
-
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="WEB_MODULE" version="4">
3
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
4
+ <exclude-output />
5
+ <content url="file://$MODULE_DIR$">
6
+ <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
7
+ <sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
8
+ <excludeFolder url="file://$MODULE_DIR$/dist" />
9
+ </content>
10
+ <orderEntry type="inheritedJdk" />
11
+ <orderEntry type="sourceFolder" forTests="false" />
12
+ </component>
13
+ </module>
@@ -3,12 +3,17 @@ import inspect
3
3
  import traceback
4
4
  import types
5
5
  import pathlib
6
+ from dataclasses import dataclass
7
+
6
8
  import pytest
7
9
  import typing
10
+ from enum import Enum
8
11
 
9
12
  from _pytest._code import ExceptionInfo
10
13
  from _pytest.config.argparsing import Parser
11
14
  from _pytest.pathlib import import_path
15
+ import logging
16
+
12
17
  from pytest_markdown_docs import hooks
13
18
 
14
19
 
@@ -22,9 +27,43 @@ else:
22
27
  if typing.TYPE_CHECKING:
23
28
  from markdown_it.token import Token
24
29
 
30
+ logger = logging.getLogger("pytest-markdown-docs")
31
+
25
32
  MARKER_NAME = "markdown-docs"
26
33
 
27
34
 
35
+ class FenceSyntax(Enum):
36
+ default = "default"
37
+ superfences = "superfences"
38
+
39
+
40
+ @dataclass
41
+ class FenceTest:
42
+ source: str
43
+ fixture_names: typing.List[str]
44
+ start_line: int
45
+
46
+
47
+ @dataclass
48
+ class ObjectTest:
49
+ intra_object_index: int
50
+ object_name: str
51
+ fence_test: FenceTest
52
+
53
+
54
+ def get_docstring_start_line(obj) -> typing.Optional[int]:
55
+ # Get the source lines and the starting line number of the object
56
+ source_lines, start_line = inspect.getsourcelines(obj)
57
+
58
+ # Find the line in the source code that starts with triple quotes (""" or ''')
59
+ for idx, line in enumerate(source_lines):
60
+ line = line.strip()
61
+ if line.startswith(('"""', "'''")):
62
+ return start_line + idx # Return the starting line number
63
+
64
+ return None # Docstring not found in source
65
+
66
+
28
67
  class MarkdownInlinePythonItem(pytest.Item):
29
68
  def __init__(
30
69
  self,
@@ -33,7 +72,6 @@ class MarkdownInlinePythonItem(pytest.Item):
33
72
  code: str,
34
73
  fixture_names: typing.List[str],
35
74
  start_line: int,
36
- fake_line_numbers: bool,
37
75
  ) -> None:
38
76
  super().__init__(name, parent)
39
77
  self.add_marker(MARKER_NAME)
@@ -41,7 +79,6 @@ class MarkdownInlinePythonItem(pytest.Item):
41
79
  self.obj = None
42
80
  self.user_properties.append(("code", code))
43
81
  self.start_line = start_line
44
- self.fake_line_numbers = fake_line_numbers
45
82
  self.fixturenames = fixture_names
46
83
  self.nofuncargs = True
47
84
 
@@ -93,58 +130,47 @@ class MarkdownInlinePythonItem(pytest.Item):
93
130
  excinfo: ExceptionInfo[BaseException],
94
131
  style=None,
95
132
  ):
96
- rawlines = self.code.split("\n")
133
+ rawlines = self.code.rstrip("\n").split("\n")
97
134
 
98
135
  # custom formatted traceback to translate line numbers and markdown files
99
136
  traceback_lines = []
100
137
  stack_summary = traceback.StackSummary.extract(traceback.walk_tb(excinfo.tb))
101
138
  start_capture = False
102
139
 
103
- start_line = 0 if self.fake_line_numbers else self.start_line
140
+ start_line = self.start_line
104
141
 
105
142
  for frame_summary in stack_summary:
106
143
  if frame_summary.filename == str(self.path):
107
- lineno = (frame_summary.lineno or 0) + start_line
108
- start_capture = (
109
- True # start capturing frames the first time we enter user code
110
- )
111
- line = (
112
- rawlines[frame_summary.lineno - 1] if frame_summary.lineno else ""
113
- )
114
- else:
115
- lineno = frame_summary.lineno or 0
116
- line = frame_summary.line or ""
144
+ # start capturing frames the first time we enter user code
145
+ start_capture = True
117
146
 
118
147
  if start_capture:
148
+ lineno = frame_summary.lineno
149
+ line = frame_summary.line or ""
119
150
  linespec = f"line {lineno}"
120
- if self.fake_line_numbers:
121
- linespec = f"code block line {lineno}*"
122
-
123
151
  traceback_lines.append(
124
152
  f""" File "{frame_summary.filename}", {linespec}, in {frame_summary.name}"""
125
153
  )
126
154
  traceback_lines.append(f" {line.lstrip()}")
127
155
 
128
- maxnum = len(str(len(rawlines) + start_line + 1))
156
+ maxdigits = len(str(len(rawlines)))
157
+ code_margin = " "
129
158
  numbered_code = "\n".join(
130
159
  [
131
- f"{i:>{maxnum}} {line}"
132
- for i, line in enumerate(rawlines, start_line + 1)
160
+ f"{i:>{maxdigits}}{code_margin}{line}"
161
+ for i, line in enumerate(rawlines[start_line:], start_line + 1)
133
162
  ]
134
163
  )
135
164
 
136
165
  pretty_traceback = "\n".join(traceback_lines)
137
- note = ""
138
- if self.fake_line_numbers:
139
- note = ", *-denoted line numbers refer to code block"
140
- pt = f"""Traceback (most recent call last{note}):
166
+ pt = f"""Traceback (most recent call last):
141
167
  {pretty_traceback}
142
168
  {excinfo.exconly()}"""
143
169
 
144
170
  return f"""Error in code block:
145
- ```
171
+ {maxdigits * " "}{code_margin}```
146
172
  {numbered_code}
147
- ```
173
+ {maxdigits * " "}{code_margin}```
148
174
  {pt}
149
175
  """
150
176
 
@@ -152,10 +178,12 @@ class MarkdownInlinePythonItem(pytest.Item):
152
178
  return self.name, 0, self.nodeid
153
179
 
154
180
 
155
- def extract_code_blocks(
181
+ def extract_fence_tests(
156
182
  markdown_string: str,
183
+ start_line_offset: int,
157
184
  markdown_type: str = "md",
158
- ) -> typing.Generator[typing.Tuple[str, typing.List[str], int], None, None]:
185
+ fence_syntax: FenceSyntax = FenceSyntax.default,
186
+ ) -> typing.Generator[FenceTest, None, None]:
159
187
  import markdown_it
160
188
 
161
189
  mi = markdown_it.MarkdownIt(config="commonmark")
@@ -166,8 +194,10 @@ def extract_code_blocks(
166
194
  if block.type != "fence" or not block.map:
167
195
  continue
168
196
 
169
- startline = block.map[0] + 1 # skip the info line when counting
170
- code_info = block.info.split()
197
+ if fence_syntax == FenceSyntax.superfences:
198
+ code_info = parse_superfences_block_info(block.info)
199
+ else:
200
+ code_info = block.info.split()
171
201
 
172
202
  lang = code_info[0] if code_info else None
173
203
  code_options = set(code_info) - {lang}
@@ -187,19 +217,50 @@ def extract_code_blocks(
187
217
  code_options |= extract_options_from_mdx_comment(tokens[i - 2].content)
188
218
 
189
219
  if lang in ("py", "python", "python3") and "notest" not in code_options:
190
- code_block = block.content
220
+ start_line = (
221
+ start_line_offset + block.map[0] + 1
222
+ ) # actual code starts on +1 from the "info" line
223
+ if "continuation" not in code_options:
224
+ prev = ""
191
225
 
192
- if "continuation" in code_options:
193
- code_block = prev + code_block
194
- startline = -1 # this disables proper line numbers, TODO: adjust line numbers *per snippet*
226
+ add_blank_lines = start_line - prev.count("\n")
227
+ code_block = prev + ("\n" * add_blank_lines) + block.content
195
228
 
196
229
  fixture_names = [
197
230
  f[len("fixture:") :] for f in code_options if f.startswith("fixture:")
198
231
  ]
199
- yield code_block, fixture_names, startline
232
+ yield FenceTest(code_block, fixture_names, start_line)
200
233
  prev = code_block
201
234
 
202
235
 
236
+ def parse_superfences_block_info(block_info: str) -> typing.List[str]:
237
+ """Parse PyMdown Superfences block info syntax.
238
+
239
+ The default `python continuation` format is not compatible with Material for Mkdocs.
240
+ But, PyMdown Superfences has a special brace format to add options to code fence blocks: `{.<lang> <option1> <option2>}`.
241
+
242
+ This function also works if the default syntax is used to allow for mixed usage.
243
+ """
244
+ block_info = block_info.strip()
245
+
246
+ if not block_info.startswith("{"):
247
+ # default syntax
248
+ return block_info.split()
249
+
250
+ block_info = block_info.strip("{}")
251
+ code_info = block_info.split()
252
+ # Lang may not be the first but is always the first element that starts with a dot.
253
+ # (https://facelessuser.github.io/pymdown-extensions/extensions/superfences/#injecting-classes-ids-and-attributes)
254
+ dot_lang = next(
255
+ (info_part for info_part in code_info if info_part.startswith(".")), None
256
+ )
257
+ if dot_lang:
258
+ code_info.remove(dot_lang)
259
+ lang = dot_lang[1:]
260
+ code_info.insert(0, lang)
261
+ return code_info
262
+
263
+
203
264
  def is_mdx_comment(block: "Token") -> bool:
204
265
  return (
205
266
  block.type == "inline"
@@ -219,29 +280,6 @@ def extract_options_from_mdx_comment(comment: str) -> typing.Set[str]:
219
280
  return set(option.strip() for option in comment.split(" ") if option)
220
281
 
221
282
 
222
- def find_object_tests_recursive(
223
- module_name: str, object: typing.Any
224
- ) -> typing.Generator[
225
- typing.Tuple[int, typing.Any, typing.Tuple[str, typing.List[str], int]], None, None
226
- ]:
227
- docstr = inspect.getdoc(object)
228
-
229
- if docstr:
230
- for i, code_block in enumerate(extract_code_blocks(docstr)):
231
- yield i, object, code_block
232
-
233
- for member_name, member in inspect.getmembers(object):
234
- if member_name.startswith("_"):
235
- continue
236
-
237
- if (
238
- inspect.isclass(member)
239
- or inspect.isfunction(member)
240
- or inspect.ismethod(member)
241
- ) and member.__module__ == module_name:
242
- yield from find_object_tests_recursive(module_name, member)
243
-
244
-
245
283
  class MarkdownDocstringCodeModule(pytest.Module):
246
284
  def collect(self):
247
285
  if pytest.version_tuple >= (8, 1, 0):
@@ -250,45 +288,74 @@ class MarkdownDocstringCodeModule(pytest.Module):
250
288
  self.path, root=self.config.rootpath, consider_namespace_packages=True
251
289
  )
252
290
  else:
253
- # but unsupported before 8.1...
291
+ # but unsupported before pytest 8.1...
254
292
  module = import_path(self.path, root=self.config.rootpath)
255
293
 
256
- for code_block_idx, obj, (
257
- test_code,
258
- fixture_names,
259
- start_line,
260
- ) in find_object_tests_recursive(module.__name__, module):
261
- obj_name = (
262
- getattr(obj, "__qualname__", None)
263
- or getattr(obj, "__name__", None)
264
- or "<Unnamed obj>"
265
- )
294
+ for object_test in self.find_object_tests_recursive(module.__name__, module):
295
+ fence_test = object_test.fence_test
266
296
  yield MarkdownInlinePythonItem.from_parent(
267
297
  self,
268
- name=f"{obj_name}[CodeBlock#{code_block_idx+1}][rel.line:{start_line}]",
269
- code=test_code,
270
- fixture_names=fixture_names,
271
- start_line=start_line,
272
- fake_line_numbers=True, # TODO: figure out where docstrings are in file to offset line numbers properly
298
+ name=f"{object_test.object_name}[CodeFence#{object_test.intra_object_index+1}][line:{fence_test.start_line}]",
299
+ code=fence_test.source,
300
+ fixture_names=fence_test.fixture_names,
301
+ start_line=fence_test.start_line,
273
302
  )
274
303
 
304
+ def find_object_tests_recursive(
305
+ self, module_name: str, object: typing.Any
306
+ ) -> typing.Generator[ObjectTest, None, None]:
307
+ docstr = inspect.getdoc(object)
308
+
309
+ if docstr:
310
+ docstring_offset = get_docstring_start_line(object)
311
+ if docstring_offset is None:
312
+ logger.warning(
313
+ "Could not find line number offset for docstring: {docstr}"
314
+ )
315
+ docstring_offset = 0
316
+
317
+ obj_name = (
318
+ getattr(object, "__qualname__", None)
319
+ or getattr(object, "__name__", None)
320
+ or "<Unnamed obj>"
321
+ )
322
+ fence_syntax = FenceSyntax(self.config.option.markdowndocs_syntax)
323
+ for i, fence_test in enumerate(
324
+ extract_fence_tests(docstr, docstring_offset, fence_syntax=fence_syntax)
325
+ ):
326
+ yield ObjectTest(i, obj_name, fence_test)
327
+
328
+ for member_name, member in inspect.getmembers(object):
329
+ if member_name.startswith("_"):
330
+ continue
331
+
332
+ if (
333
+ inspect.isclass(member)
334
+ or inspect.isfunction(member)
335
+ or inspect.ismethod(member)
336
+ ) and member.__module__ == module_name:
337
+ yield from self.find_object_tests_recursive(module_name, member)
338
+
275
339
 
276
340
  class MarkdownTextFile(pytest.File):
277
341
  def collect(self):
278
342
  markdown_content = self.path.read_text("utf8")
279
-
280
- for i, (code_block, fixture_names, start_line) in enumerate(
281
- extract_code_blocks(
282
- markdown_content, markdown_type=self.path.suffix.replace(".", "")
343
+ fence_syntax = FenceSyntax(self.config.option.markdowndocs_syntax)
344
+
345
+ for i, fence_test in enumerate(
346
+ extract_fence_tests(
347
+ markdown_content,
348
+ start_line_offset=0,
349
+ markdown_type=self.path.suffix.replace(".", ""),
350
+ fence_syntax=fence_syntax,
283
351
  )
284
352
  ):
285
353
  yield MarkdownInlinePythonItem.from_parent(
286
354
  self,
287
- name=f"[CodeBlock#{i+1}][line:{start_line}]",
288
- code=code_block,
289
- fixture_names=fixture_names,
290
- start_line=start_line,
291
- fake_line_numbers=start_line == -1,
355
+ name=f"[CodeFence#{i+1}][line:{fence_test.start_line}]",
356
+ code=fence_test.source,
357
+ fixture_names=fence_test.fixture_names,
358
+ start_line=fence_test.start_line,
292
359
  )
293
360
 
294
361
 
@@ -321,6 +388,14 @@ def pytest_addoption(parser: Parser) -> None:
321
388
  help="run ",
322
389
  dest="markdowndocs",
323
390
  )
391
+ group.addoption(
392
+ "--markdown-docs-syntax",
393
+ action="store",
394
+ choices=[choice.value for choice in FenceSyntax],
395
+ default="default",
396
+ help="Choose an alternative fences syntax",
397
+ dest="markdowndocs_syntax",
398
+ )
324
399
 
325
400
 
326
401
  def pytest_addhooks(pluginmanager):
@@ -0,0 +1,10 @@
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,4 +1,7 @@
1
1
  import re
2
+
3
+ from _pytest.pytester import LineMatcher
4
+
2
5
  import pytest_markdown_docs # hack: used for storing a side effect in one of the tests
3
6
 
4
7
 
@@ -122,7 +125,7 @@ def test_traceback(testdir):
122
125
  # we check the traceback vs a regex pattern since the file paths can change
123
126
  expected_output_pattern = r"""
124
127
  Error in code block:
125
- ```
128
+ ```
126
129
  4 def foo\(\):
127
130
  5 raise Exception\("doh"\)
128
131
  6
@@ -130,8 +133,7 @@ Error in code block:
130
133
  8 foo\(\)
131
134
  9
132
135
  10 foo\(\)
133
- 11
134
- ```
136
+ ```
135
137
  Traceback \(most recent call last\):
136
138
  File ".*/test_traceback.md", line 10, in <module>
137
139
  foo\(\)
@@ -350,3 +352,82 @@ def test_notest_mdx_comment(testdir):
350
352
  )
351
353
  result = testdir.runpytest("--markdown-docs")
352
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
+ )
@@ -0,0 +1,11 @@
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")
@@ -0,0 +1,11 @@
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
+ """
@@ -1,410 +0,0 @@
1
- # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
2
-
3
- [[package]]
4
- name = "cfgv"
5
- version = "3.4.0"
6
- description = "Validate configuration and produce human readable error messages."
7
- optional = false
8
- python-versions = ">=3.8"
9
- files = [
10
- {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
11
- {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
12
- ]
13
-
14
- [[package]]
15
- name = "colorama"
16
- version = "0.4.6"
17
- description = "Cross-platform colored terminal text."
18
- optional = false
19
- python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
20
- files = [
21
- {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
22
- {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
23
- ]
24
-
25
- [[package]]
26
- name = "distlib"
27
- version = "0.3.8"
28
- description = "Distribution utilities"
29
- optional = false
30
- python-versions = "*"
31
- files = [
32
- {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
33
- {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
34
- ]
35
-
36
- [[package]]
37
- name = "exceptiongroup"
38
- version = "1.2.2"
39
- description = "Backport of PEP 654 (exception groups)"
40
- optional = false
41
- python-versions = ">=3.7"
42
- files = [
43
- {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
44
- {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
45
- ]
46
-
47
- [package.extras]
48
- test = ["pytest (>=6)"]
49
-
50
- [[package]]
51
- name = "filelock"
52
- version = "3.16.1"
53
- description = "A platform independent file lock."
54
- optional = false
55
- python-versions = ">=3.8"
56
- files = [
57
- {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"},
58
- {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"},
59
- ]
60
-
61
- [package.extras]
62
- docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"]
63
- testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"]
64
- typing = ["typing-extensions (>=4.12.2)"]
65
-
66
- [[package]]
67
- name = "identify"
68
- version = "2.6.1"
69
- description = "File identification library for Python"
70
- optional = false
71
- python-versions = ">=3.8"
72
- files = [
73
- {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"},
74
- {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"},
75
- ]
76
-
77
- [package.extras]
78
- license = ["ukkonen"]
79
-
80
- [[package]]
81
- name = "iniconfig"
82
- version = "2.0.0"
83
- description = "brain-dead simple config-ini parsing"
84
- optional = false
85
- python-versions = ">=3.7"
86
- files = [
87
- {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
88
- {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
89
- ]
90
-
91
- [[package]]
92
- name = "markdown-it-py"
93
- version = "3.0.0"
94
- description = "Python port of markdown-it. Markdown parsing, done right!"
95
- optional = false
96
- python-versions = ">=3.8"
97
- files = [
98
- {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
99
- {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
100
- ]
101
-
102
- [package.dependencies]
103
- mdurl = ">=0.1,<1.0"
104
-
105
- [package.extras]
106
- benchmarking = ["psutil", "pytest", "pytest-benchmark"]
107
- code-style = ["pre-commit (>=3.0,<4.0)"]
108
- compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
109
- linkify = ["linkify-it-py (>=1,<3)"]
110
- plugins = ["mdit-py-plugins"]
111
- profiling = ["gprof2dot"]
112
- rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
113
- testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
114
-
115
- [[package]]
116
- name = "mdurl"
117
- version = "0.1.2"
118
- description = "Markdown URL utilities"
119
- optional = false
120
- python-versions = ">=3.7"
121
- files = [
122
- {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
123
- {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
124
- ]
125
-
126
- [[package]]
127
- name = "mypy"
128
- version = "1.11.2"
129
- description = "Optional static typing for Python"
130
- optional = false
131
- python-versions = ">=3.8"
132
- files = [
133
- {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"},
134
- {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"},
135
- {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"},
136
- {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"},
137
- {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"},
138
- {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"},
139
- {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"},
140
- {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"},
141
- {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"},
142
- {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"},
143
- {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"},
144
- {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"},
145
- {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"},
146
- {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"},
147
- {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"},
148
- {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"},
149
- {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"},
150
- {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"},
151
- {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"},
152
- {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"},
153
- {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"},
154
- {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"},
155
- {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"},
156
- {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"},
157
- {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"},
158
- {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"},
159
- {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"},
160
- ]
161
-
162
- [package.dependencies]
163
- mypy-extensions = ">=1.0.0"
164
- tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
165
- typing-extensions = ">=4.6.0"
166
-
167
- [package.extras]
168
- dmypy = ["psutil (>=4.0)"]
169
- install-types = ["pip"]
170
- mypyc = ["setuptools (>=50)"]
171
- reports = ["lxml"]
172
-
173
- [[package]]
174
- name = "mypy-extensions"
175
- version = "1.0.0"
176
- description = "Type system extensions for programs checked with the mypy type checker."
177
- optional = false
178
- python-versions = ">=3.5"
179
- files = [
180
- {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
181
- {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
182
- ]
183
-
184
- [[package]]
185
- name = "nodeenv"
186
- version = "1.9.1"
187
- description = "Node.js virtual environment builder"
188
- optional = false
189
- python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
190
- files = [
191
- {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
192
- {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
193
- ]
194
-
195
- [[package]]
196
- name = "packaging"
197
- version = "24.1"
198
- description = "Core utilities for Python packages"
199
- optional = false
200
- python-versions = ">=3.8"
201
- files = [
202
- {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
203
- {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
204
- ]
205
-
206
- [[package]]
207
- name = "platformdirs"
208
- version = "4.3.6"
209
- description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
210
- optional = false
211
- python-versions = ">=3.8"
212
- files = [
213
- {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
214
- {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
215
- ]
216
-
217
- [package.extras]
218
- docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
219
- test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
220
- type = ["mypy (>=1.11.2)"]
221
-
222
- [[package]]
223
- name = "pluggy"
224
- version = "1.5.0"
225
- description = "plugin and hook calling mechanisms for python"
226
- optional = false
227
- python-versions = ">=3.8"
228
- files = [
229
- {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
230
- {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
231
- ]
232
-
233
- [package.extras]
234
- dev = ["pre-commit", "tox"]
235
- testing = ["pytest", "pytest-benchmark"]
236
-
237
- [[package]]
238
- name = "pre-commit"
239
- version = "3.8.0"
240
- description = "A framework for managing and maintaining multi-language pre-commit hooks."
241
- optional = false
242
- python-versions = ">=3.9"
243
- files = [
244
- {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
245
- {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
246
- ]
247
-
248
- [package.dependencies]
249
- cfgv = ">=2.0.0"
250
- identify = ">=1.0.0"
251
- nodeenv = ">=0.11.1"
252
- pyyaml = ">=5.1"
253
- virtualenv = ">=20.10.0"
254
-
255
- [[package]]
256
- name = "pytest"
257
- version = "8.3.3"
258
- description = "pytest: simple powerful testing with Python"
259
- optional = false
260
- python-versions = ">=3.8"
261
- files = [
262
- {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
263
- {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
264
- ]
265
-
266
- [package.dependencies]
267
- colorama = {version = "*", markers = "sys_platform == \"win32\""}
268
- exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
269
- iniconfig = "*"
270
- packaging = "*"
271
- pluggy = ">=1.5,<2"
272
- tomli = {version = ">=1", markers = "python_version < \"3.11\""}
273
-
274
- [package.extras]
275
- dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
276
-
277
- [[package]]
278
- name = "pyyaml"
279
- version = "6.0.2"
280
- description = "YAML parser and emitter for Python"
281
- optional = false
282
- python-versions = ">=3.8"
283
- files = [
284
- {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
285
- {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
286
- {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"},
287
- {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"},
288
- {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"},
289
- {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"},
290
- {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"},
291
- {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"},
292
- {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"},
293
- {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"},
294
- {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"},
295
- {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"},
296
- {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"},
297
- {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"},
298
- {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"},
299
- {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"},
300
- {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"},
301
- {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"},
302
- {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"},
303
- {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"},
304
- {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"},
305
- {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"},
306
- {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"},
307
- {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"},
308
- {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"},
309
- {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"},
310
- {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"},
311
- {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"},
312
- {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"},
313
- {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"},
314
- {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"},
315
- {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"},
316
- {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"},
317
- {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"},
318
- {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"},
319
- {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"},
320
- {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"},
321
- {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"},
322
- {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"},
323
- {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"},
324
- {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"},
325
- {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"},
326
- {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"},
327
- {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"},
328
- {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"},
329
- {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"},
330
- {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"},
331
- {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"},
332
- {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"},
333
- {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"},
334
- {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"},
335
- {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"},
336
- {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
337
- ]
338
-
339
- [[package]]
340
- name = "ruff"
341
- version = "0.2.2"
342
- description = "An extremely fast Python linter and code formatter, written in Rust."
343
- optional = false
344
- python-versions = ">=3.7"
345
- files = [
346
- {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"},
347
- {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"},
348
- {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"},
349
- {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"},
350
- {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"},
351
- {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"},
352
- {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"},
353
- {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"},
354
- {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"},
355
- {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"},
356
- {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"},
357
- {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"},
358
- {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"},
359
- {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"},
360
- {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"},
361
- {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"},
362
- {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"},
363
- ]
364
-
365
- [[package]]
366
- name = "tomli"
367
- version = "2.0.1"
368
- description = "A lil' TOML parser"
369
- optional = false
370
- python-versions = ">=3.7"
371
- files = [
372
- {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
373
- {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
374
- ]
375
-
376
- [[package]]
377
- name = "typing-extensions"
378
- version = "4.12.2"
379
- description = "Backported and Experimental Type Hints for Python 3.8+"
380
- optional = false
381
- python-versions = ">=3.8"
382
- files = [
383
- {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
384
- {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
385
- ]
386
-
387
- [[package]]
388
- name = "virtualenv"
389
- version = "20.26.5"
390
- description = "Virtual Python Environment builder"
391
- optional = false
392
- python-versions = ">=3.7"
393
- files = [
394
- {file = "virtualenv-20.26.5-py3-none-any.whl", hash = "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6"},
395
- {file = "virtualenv-20.26.5.tar.gz", hash = "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4"},
396
- ]
397
-
398
- [package.dependencies]
399
- distlib = ">=0.3.7,<1"
400
- filelock = ">=3.12.2,<4"
401
- platformdirs = ">=3.9.1,<5"
402
-
403
- [package.extras]
404
- docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
405
- test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
406
-
407
- [metadata]
408
- lock-version = "2.0"
409
- python-versions = "^3.8"
410
- content-hash = "555416233d534c92e9fd0327b7024f9185a43aeb4c96ba51d1b3b84c9f7633f9"
@@ -1,3 +0,0 @@
1
- [virtualenvs]
2
- prefer-active-python = true
3
- in-project = true
@@ -1 +0,0 @@
1
- pytest_plugins = ["pytester"]