Skip to content

Commit cb98df2

Browse files
committed
ruff_python_formatter: finish inital support for doctest formatting
This connects all of the pieces together from the previous commit and makes the docstring line printer reformat doctest code snippets. This also includes a new (and possibly first?) recursive call to the formatter, so extra scrutiny there is most appreciated.
1 parent e768fba commit cb98df2

File tree

4 files changed

+3633
-28
lines changed

4 files changed

+3633
-28
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
[
2+
{
3+
"docstring_code": "disabled",
4+
"indent_style": "space",
5+
"indent_width": 4
6+
},
7+
{
8+
"docstring_code": "disabled",
9+
"indent_style": "space",
10+
"indent_width": 2
11+
},
12+
{
13+
"docstring_code": "disabled",
14+
"indent_style": "tab",
15+
"indent_width": 8
16+
},
17+
{
18+
"docstring_code": "disabled",
19+
"indent_style": "tab",
20+
"indent_width": 4
21+
},
22+
{
23+
"docstring_code": "enabled",
24+
"indent_style": "space",
25+
"indent_width": 4
26+
},
27+
{
28+
"docstring_code": "enabled",
29+
"indent_style": "space",
30+
"indent_width": 2
31+
},
32+
{
33+
"docstring_code": "enabled",
34+
"indent_style": "tab",
35+
"indent_width": 8
36+
},
37+
{
38+
"docstring_code": "enabled",
39+
"indent_style": "tab",
40+
"indent_width": 4
41+
}
42+
]
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
# The simplest doctest to ensure basic formatting works.
2+
def doctest_simple():
3+
"""
4+
Do cool stuff.
5+
6+
>>> cool_stuff( 1 )
7+
2
8+
"""
9+
pass
10+
11+
12+
# Another simple test, but one where the Python code
13+
# extends over multiple lines.
14+
def doctest_simple_continued():
15+
"""
16+
Do cool stuff.
17+
18+
>>> def cool_stuff( x ):
19+
... print( f"hi {x}" );
20+
hi 2
21+
"""
22+
pass
23+
24+
25+
# Test that we support multiple directly adjacent
26+
# doctests.
27+
def doctest_adjacent():
28+
"""
29+
Do cool stuff.
30+
31+
>>> cool_stuff( x )
32+
>>> cool_stuff( y )
33+
2
34+
"""
35+
pass
36+
37+
38+
# Test that a doctest on the last non-whitespace line of a docstring
39+
# reformats correctly.
40+
def doctest_last_line():
41+
"""
42+
Do cool stuff.
43+
44+
>>> cool_stuff( x )
45+
"""
46+
pass
47+
48+
49+
# Test that a doctest that continues to the last non-whitespace line of
50+
# a docstring reformats correctly.
51+
def doctest_last_line_continued():
52+
"""
53+
Do cool stuff.
54+
55+
>>> def cool_stuff( x ):
56+
... print( f"hi {x}" );
57+
"""
58+
pass
59+
60+
61+
# Test that a doctest is correctly identified and formatted with a blank
62+
# continuation line.
63+
def doctest_blank_continued():
64+
"""
65+
Do cool stuff.
66+
67+
>>> def cool_stuff ( x ):
68+
... print( x )
69+
...
70+
... print( x )
71+
"""
72+
pass
73+
74+
75+
# Tests that a blank PS2 line at the end of a doctest can get dropped.
76+
# It is treated as part of the Python snippet which will trim the
77+
# trailing whitespace.
78+
def doctest_blank_end():
79+
"""
80+
Do cool stuff.
81+
82+
>>> def cool_stuff ( x ):
83+
... print( x )
84+
... print( x )
85+
...
86+
"""
87+
pass
88+
89+
90+
# Tests that a blank PS2 line at the end of a doctest can get dropped
91+
# even when there is text following it.
92+
def doctest_blank_end_then_some_text():
93+
"""
94+
Do cool stuff.
95+
96+
>>> def cool_stuff ( x ):
97+
... print( x )
98+
... print( x )
99+
...
100+
101+
And say something else.
102+
"""
103+
pass
104+
105+
106+
# Test that a doctest containing a triple quoted string gets formatted
107+
# correctly and doesn't result in invalid syntax.
108+
def doctest_with_triple_single():
109+
"""
110+
Do cool stuff.
111+
112+
>>> x = '''tricksy'''
113+
"""
114+
pass
115+
116+
117+
# Test that a doctest containing a triple quoted f-string gets
118+
# formatted correctly and doesn't result in invalid syntax.
119+
def doctest_with_triple_single():
120+
"""
121+
Do cool stuff.
122+
123+
>>> x = f'''tricksy'''
124+
"""
125+
pass
126+
127+
128+
# Another nested multi-line string case, but with triple escaped double
129+
# quotes inside a triple single quoted string.
130+
def doctest_with_triple_escaped_double():
131+
"""
132+
Do cool stuff.
133+
134+
>>> x = '''\"\"\"'''
135+
"""
136+
pass
137+
138+
139+
# Tests that inverting the triple quoting works as expected.
140+
def doctest_with_triple_inverted():
141+
'''
142+
Do cool stuff.
143+
144+
>>> x = """tricksy"""
145+
'''
146+
pass
147+
148+
149+
# Tests that inverting the triple quoting with an f-string works as
150+
# expected.
151+
def doctest_with_triple_inverted_fstring():
152+
'''
153+
Do cool stuff.
154+
155+
>>> x = f"""tricksy"""
156+
'''
157+
pass
158+
159+
160+
# Tests nested doctests are ignored. That is, we don't format doctests
161+
# recursively. We only recognize "top level" doctests.
162+
#
163+
# This restriction primarily exists to avoid needing to deal with
164+
# nesting quotes. It also seems like a generally sensible restriction,
165+
# although it could be lifted if necessary I believe.
166+
def doctest_nested_doctest_not_formatted():
167+
'''
168+
Do cool stuff.
169+
170+
>>> def nested( x ):
171+
... """
172+
... Do nested cool stuff.
173+
... >>> func_call( 5 )
174+
... """
175+
... pass
176+
'''
177+
pass
178+
179+
180+
# Tests that the starting column does not matter.
181+
def doctest_varying_start_column():
182+
'''
183+
Do cool stuff.
184+
185+
>>> assert ("Easy!")
186+
>>> import math
187+
>>> math.floor( 1.9 )
188+
1
189+
'''
190+
pass
191+
192+
193+
# Tests that long lines get wrapped... appropriately.
194+
#
195+
# The docstring code formatter uses the same line width settings as for
196+
# formatting other code. This means that a line in the docstring can
197+
# actually extend past the configured line limit.
198+
#
199+
# It's not quite clear whether this is desirable or not. We could in
200+
# theory compute the intendation length of a code snippet and then
201+
# adjust the line-width setting on a recursive call to the formatter.
202+
# But there are assuredly pathological cases to consider. Another path
203+
# would be to expose another formatter option for controlling the
204+
# line-width of code snippets independently.
205+
def doctest_long_lines():
206+
'''
207+
Do cool stuff.
208+
209+
This won't get wrapped even though it exceeds our configured
210+
line width because it doesn't exceed the line width within this
211+
docstring. e.g, the `f` in `foo` is treated as the first column.
212+
>>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)
213+
214+
But this one is long enough to get wrapped.
215+
>>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard)
216+
'''
217+
# This demostrates a normal line that will get wrapped but won't
218+
# get wrapped in the docstring above because of how the line-width
219+
# setting gets reset at the first column in each code snippet.
220+
foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)
221+
222+
223+
# Checks that a simple but invalid doctest gets skipped.
224+
def doctest_skipped_simple():
225+
"""
226+
Do cool stuff.
227+
228+
>>> cool-stuff( x ):
229+
2
230+
"""
231+
pass
232+
233+
234+
# Checks that a simple doctest that is continued over multiple lines,
235+
# but is invalid, gets skipped.
236+
def doctest_skipped_simple_continued():
237+
"""
238+
Do cool stuff.
239+
240+
>>> def cool-stuff( x ):
241+
... print( f"hi {x}" );
242+
2
243+
"""
244+
pass
245+
246+
247+
# Checks that a doctest with improper indentation gets skipped.
248+
def doctest_skipped_inconsistent_indent():
249+
"""
250+
Do cool stuff.
251+
252+
>>> def cool_stuff( x ):
253+
... print( f"hi {x}" );
254+
hi 2
255+
"""
256+
pass
257+
258+
# Checks that a doctest with some proper indentation and some improper
259+
# indentation is "partially" formatted. That is, the part that appears
260+
# before the inconsistent indentation is formatted. This requires that
261+
# the part before it is valid Python.
262+
def doctest_skipped_partial_inconsistent_indent():
263+
"""
264+
Do cool stuff.
265+
266+
>>> def cool_stuff( x ):
267+
... print( x )
268+
... print( f"hi {x}" );
269+
hi 2
270+
"""
271+
pass
272+
273+
274+
# Checks that a doctest with improper triple single
275+
# quoted string gets skipped.
276+
def doctest_skipped_triple_incorrect():
277+
"""
278+
Do cool stuff.
279+
280+
>>> foo( x )
281+
... '''tri'''cksy'''
282+
"""
283+
pass
284+
285+
286+
# Tests that a doctest on a single line is skipped.
287+
def doctest_skipped_one_line():
288+
">>> foo( x )"
289+
pass
290+
291+
292+
# f-strings are not considered docstrings[1], so any doctests
293+
# inside of them should not be formatted.
294+
#
295+
# [1]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals
296+
def doctest_skipped_fstring():
297+
f"""
298+
Do cool stuff.
299+
300+
>>> cool_stuff( 1 )
301+
2
302+
"""
303+
pass
304+
305+
306+
# Test that a doctest containing a triple quoted string at least
307+
# does not result in invalid Python code. Ideally this would format
308+
# correctly, but at time of writing it does not.
309+
def doctest_invalid_skipped_with_triple_double_in_single_quote_string():
310+
"""
311+
Do cool stuff.
312+
313+
>>> x = '\"\"\"'
314+
"""
315+
pass

0 commit comments

Comments
 (0)