@@ -35,7 +35,7 @@ class FenceSyntax(Enum):
35
35
36
36
@dataclass
37
37
class FenceTest :
38
- code_block : str
38
+ source : str
39
39
fixture_names : typing .List [str ]
40
40
start_line : int
41
41
@@ -47,6 +47,19 @@ class ObjectTest:
47
47
fence_test : FenceTest
48
48
49
49
50
+ def get_docstring_start_line (obj ) -> int :
51
+ # Get the source lines and the starting line number of the object
52
+ source_lines , start_line = inspect .getsourcelines (obj )
53
+
54
+ # Find the line in the source code that starts with triple quotes (""" or ''')
55
+ for idx , line in enumerate (source_lines ):
56
+ line = line .strip ()
57
+ if line .startswith (('"""' , "'''" )):
58
+ return start_line + idx # Return the starting line number
59
+
60
+ return None # Docstring not found in source
61
+
62
+
50
63
class MarkdownInlinePythonItem (pytest .Item ):
51
64
def __init__ (
52
65
self ,
@@ -55,15 +68,13 @@ def __init__(
55
68
code : str ,
56
69
fixture_names : typing .List [str ],
57
70
start_line : int ,
58
- fake_line_numbers : bool ,
59
71
) -> None :
60
72
super ().__init__ (name , parent )
61
73
self .add_marker (MARKER_NAME )
62
74
self .code = code
63
75
self .obj = None
64
76
self .user_properties .append (("code" , code ))
65
77
self .start_line = start_line
66
- self .fake_line_numbers = fake_line_numbers
67
78
self .fixturenames = fixture_names
68
79
self .nofuncargs = True
69
80
@@ -115,61 +126,47 @@ def repr_failure(
115
126
excinfo : ExceptionInfo [BaseException ],
116
127
style = None ,
117
128
):
118
- rawlines = self .code .split ("\n " )
129
+ rawlines = self .code .rstrip ( " \n " ). split ("\n " )
119
130
120
131
# custom formatted traceback to translate line numbers and markdown files
121
132
traceback_lines = []
122
133
stack_summary = traceback .StackSummary .extract (traceback .walk_tb (excinfo .tb ))
123
134
start_capture = False
124
135
125
- start_line = 0 if self . fake_line_numbers else self .start_line
136
+ start_line = self .start_line
126
137
127
138
for frame_summary in stack_summary :
128
139
if frame_summary .filename == str (self .path ):
129
- lineno = (frame_summary .lineno or 0 ) + start_line
130
- start_capture = (
131
- True # start capturing frames the first time we enter user code
132
- )
133
- line = (
134
- rawlines [frame_summary .lineno - 1 ]
135
- if frame_summary .lineno is not None
136
- and 1 <= frame_summary .lineno <= len (rawlines )
137
- else ""
138
- )
139
- else :
140
- lineno = frame_summary .lineno or 0
141
- line = frame_summary .line or ""
140
+ # start capturing frames the first time we enter user code
141
+ start_capture = True
142
142
143
143
if start_capture :
144
+ lineno = frame_summary .lineno
145
+ line = frame_summary .line
144
146
linespec = f"line { lineno } "
145
- if self .fake_line_numbers :
146
- linespec = f"code block line { lineno } *"
147
-
148
147
traceback_lines .append (
149
148
f""" File "{ frame_summary .filename } ", { linespec } , in { frame_summary .name } """
150
149
)
151
150
traceback_lines .append (f" { line .lstrip ()} " )
152
151
153
- maxnum = len (str (len (rawlines ) + start_line + 1 ))
152
+ maxdigits = len (str (len (rawlines )))
153
+ code_margin = " "
154
154
numbered_code = "\n " .join (
155
155
[
156
- f"{ i :>{maxnum } } { line } "
157
- for i , line in enumerate (rawlines , start_line + 1 )
156
+ f"{ i :>{maxdigits } } { code_margin } { line } "
157
+ for i , line in enumerate (rawlines [ start_line :] , start_line + 1 )
158
158
]
159
159
)
160
160
161
161
pretty_traceback = "\n " .join (traceback_lines )
162
- note = ""
163
- if self .fake_line_numbers :
164
- note = ", *-denoted line numbers refer to code block"
165
- pt = f"""Traceback (most recent call last{ note } ):
162
+ pt = f"""Traceback (most recent call last):
166
163
{ pretty_traceback }
167
164
{ excinfo .exconly ()} """
168
165
169
166
return f"""Error in code block:
170
- ```
167
+ { maxdigits * " " } { code_margin } ```
171
168
{ numbered_code }
172
- ```
169
+ { maxdigits * " " } { code_margin } ```
173
170
{ pt }
174
171
"""
175
172
@@ -179,6 +176,7 @@ def reportinfo(self):
179
176
180
177
def extract_fence_tests (
181
178
markdown_string : str ,
179
+ start_line_offset : int ,
182
180
markdown_type : str = "md" ,
183
181
fence_syntax : FenceSyntax = FenceSyntax .default ,
184
182
) -> typing .Generator [FenceTest , None , None ]:
@@ -192,7 +190,6 @@ def extract_fence_tests(
192
190
if block .type != "fence" or not block .map :
193
191
continue
194
192
195
- start_line = block .map [0 ] + 1 # skip the info line when counting
196
193
if fence_syntax == FenceSyntax .superfences :
197
194
code_info = parse_superfences_block_info (block .info )
198
195
else :
@@ -216,11 +213,14 @@ def extract_fence_tests(
216
213
code_options |= extract_options_from_mdx_comment (tokens [i - 2 ].content )
217
214
218
215
if lang in ("py" , "python" , "python3" ) and "notest" not in code_options :
219
- code_block = block .content
216
+ start_line = (
217
+ start_line_offset + block .map [0 ] + 1
218
+ ) # actual code starts on +1 from the "info" line
219
+ if "continuation" not in code_options :
220
+ prev = ""
220
221
221
- if "continuation" in code_options :
222
- code_block = prev + code_block
223
- start_line = - 1 # this disables proper line numbers, TODO: adjust line numbers *per snippet*
222
+ add_blank_lines = start_line - prev .count ("\n " )
223
+ code_block = prev + ("\n " * add_blank_lines ) + block .content
224
224
225
225
fixture_names = [
226
226
f [len ("fixture:" ) :] for f in code_options if f .startswith ("fixture:" )
@@ -291,11 +291,10 @@ def collect(self):
291
291
fence_test = object_test .fence_test
292
292
yield MarkdownInlinePythonItem .from_parent (
293
293
self ,
294
- name = f"{ object_test .object_name } [CodeBlock #{ object_test .intra_object_index + 1 } ][rel. line:{ fence_test .start_line } ]" ,
295
- code = fence_test .code_block ,
294
+ name = f"{ object_test .object_name } [CodeFence #{ object_test .intra_object_index + 1 } ][line:{ fence_test .start_line } ]" ,
295
+ code = fence_test .source ,
296
296
fixture_names = fence_test .fixture_names ,
297
297
start_line = fence_test .start_line ,
298
- fake_line_numbers = True , # TODO: figure out where docstrings are in file to offset line numbers properly
299
298
)
300
299
301
300
def find_object_tests_recursive (
@@ -304,14 +303,15 @@ def find_object_tests_recursive(
304
303
docstr = inspect .getdoc (object )
305
304
306
305
if docstr :
306
+ docstring_offset = get_docstring_start_line (object )
307
307
obj_name = (
308
308
getattr (object , "__qualname__" , None )
309
309
or getattr (object , "__name__" , None )
310
310
or "<Unnamed obj>"
311
311
)
312
312
fence_syntax = FenceSyntax (self .config .option .markdowndocs_syntax )
313
313
for i , fence_test in enumerate (
314
- extract_fence_tests (docstr , fence_syntax = fence_syntax )
314
+ extract_fence_tests (docstr , docstring_offset , fence_syntax = fence_syntax )
315
315
):
316
316
yield ObjectTest (i , obj_name , fence_test )
317
317
@@ -335,17 +335,17 @@ def collect(self):
335
335
for i , fence_test in enumerate (
336
336
extract_fence_tests (
337
337
markdown_content ,
338
+ start_line_offset = 0 ,
338
339
markdown_type = self .path .suffix .replace ("." , "" ),
339
340
fence_syntax = fence_syntax ,
340
341
)
341
342
):
342
343
yield MarkdownInlinePythonItem .from_parent (
343
344
self ,
344
- name = f"[CodeBlock #{ i + 1 } ][line:{ fence_test .start_line } ]" ,
345
- code = fence_test .code_block ,
345
+ name = f"[CodeFence #{ i + 1 } ][line:{ fence_test .start_line } ]" ,
346
+ code = fence_test .source ,
346
347
fixture_names = fence_test .fixture_names ,
347
348
start_line = fence_test .start_line ,
348
- fake_line_numbers = fence_test .start_line == - 1 ,
349
349
)
350
350
351
351
0 commit comments