Skip to content

Commit b74f3be

Browse files
authored
gh-137308: Replace a single docstring with pass in -OO mode (#137318)
This is required so we would never have empty node bodies. Refs #130087
1 parent fe0e921 commit b74f3be

File tree

3 files changed

+154
-1
lines changed

3 files changed

+154
-1
lines changed

Lib/test/test_ast/test_ast.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,131 @@ def test_negative_locations_for_compile(self):
220220
# This also must not crash:
221221
ast.parse(tree, optimize=2)
222222

223+
def test_docstring_optimization_single_node(self):
224+
# https://github.com/python/cpython/issues/137308
225+
class_example1 = textwrap.dedent('''
226+
class A:
227+
"""Docstring"""
228+
''')
229+
class_example2 = textwrap.dedent('''
230+
class A:
231+
"""
232+
Docstring"""
233+
''')
234+
def_example1 = textwrap.dedent('''
235+
def some():
236+
"""Docstring"""
237+
''')
238+
def_example2 = textwrap.dedent('''
239+
def some():
240+
"""Docstring
241+
"""
242+
''')
243+
async_def_example1 = textwrap.dedent('''
244+
async def some():
245+
"""Docstring"""
246+
''')
247+
async_def_example2 = textwrap.dedent('''
248+
async def some():
249+
"""
250+
Docstring
251+
"""
252+
''')
253+
for code in [
254+
class_example1,
255+
class_example2,
256+
def_example1,
257+
def_example2,
258+
async_def_example1,
259+
async_def_example2,
260+
]:
261+
for opt_level in [0, 1, 2]:
262+
with self.subTest(code=code, opt_level=opt_level):
263+
mod = ast.parse(code, optimize=opt_level)
264+
self.assertEqual(len(mod.body[0].body), 1)
265+
if opt_level == 2:
266+
pass_stmt = mod.body[0].body[0]
267+
self.assertIsInstance(pass_stmt, ast.Pass)
268+
self.assertEqual(
269+
vars(pass_stmt),
270+
{
271+
'lineno': 3,
272+
'col_offset': 4,
273+
'end_lineno': 3,
274+
'end_col_offset': 8,
275+
},
276+
)
277+
else:
278+
self.assertIsInstance(mod.body[0].body[0], ast.Expr)
279+
self.assertIsInstance(
280+
mod.body[0].body[0].value,
281+
ast.Constant,
282+
)
283+
284+
compile(code, "a", "exec")
285+
compile(code, "a", "exec", optimize=opt_level)
286+
compile(mod, "a", "exec")
287+
compile(mod, "a", "exec", optimize=opt_level)
288+
289+
def test_docstring_optimization_multiple_nodes(self):
290+
# https://github.com/python/cpython/issues/137308
291+
class_example = textwrap.dedent(
292+
"""
293+
class A:
294+
'''
295+
Docstring
296+
'''
297+
x = 1
298+
"""
299+
)
300+
301+
def_example = textwrap.dedent(
302+
"""
303+
def some():
304+
'''
305+
Docstring
306+
307+
'''
308+
x = 1
309+
"""
310+
)
311+
312+
async_def_example = textwrap.dedent(
313+
"""
314+
async def some():
315+
316+
'''Docstring
317+
318+
'''
319+
x = 1
320+
"""
321+
)
322+
323+
for code in [
324+
class_example,
325+
def_example,
326+
async_def_example,
327+
]:
328+
for opt_level in [0, 1, 2]:
329+
with self.subTest(code=code, opt_level=opt_level):
330+
mod = ast.parse(code, optimize=opt_level)
331+
if opt_level == 2:
332+
self.assertNotIsInstance(
333+
mod.body[0].body[0],
334+
(ast.Pass, ast.Expr),
335+
)
336+
else:
337+
self.assertIsInstance(mod.body[0].body[0], ast.Expr)
338+
self.assertIsInstance(
339+
mod.body[0].body[0].value,
340+
ast.Constant,
341+
)
342+
343+
compile(code, "a", "exec")
344+
compile(code, "a", "exec", optimize=opt_level)
345+
compile(mod, "a", "exec")
346+
compile(mod, "a", "exec", optimize=opt_level)
347+
223348
def test_slice(self):
224349
slc = ast.parse("x[::]").body[0].value.slice
225350
self.assertIsNone(slc.upper)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
A standalone docstring in a node body is optimized as a :keyword:`pass`
2+
statement to ensure that the node's body is never empty. There was a
3+
:exc:`ValueError` in :func:`compile` otherwise.

Python/ast_preprocess.c

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,13 +435,38 @@ stmt_seq_remove_item(asdl_stmt_seq *stmts, Py_ssize_t idx)
435435
return 1;
436436
}
437437

438+
static int
439+
remove_docstring(asdl_stmt_seq *stmts, Py_ssize_t idx, PyArena *ctx_)
440+
{
441+
assert(_PyAST_GetDocString(stmts) != NULL);
442+
// In case there's just the docstring in the body, replace it with `pass`
443+
// keyword, so body won't be empty.
444+
if (asdl_seq_LEN(stmts) == 1) {
445+
stmt_ty docstring = (stmt_ty)asdl_seq_GET(stmts, 0);
446+
stmt_ty pass = _PyAST_Pass(
447+
docstring->lineno, docstring->col_offset,
448+
// we know that `pass` always takes 4 chars and a single line,
449+
// while docstring can span on multiple lines
450+
docstring->lineno, docstring->col_offset + 4,
451+
ctx_
452+
);
453+
if (pass == NULL) {
454+
return 0;
455+
}
456+
asdl_seq_SET(stmts, 0, pass);
457+
return 1;
458+
}
459+
// In case there are more than 1 body items, just remove the docstring.
460+
return stmt_seq_remove_item(stmts, idx);
461+
}
462+
438463
static int
439464
astfold_body(asdl_stmt_seq *stmts, PyArena *ctx_, _PyASTPreprocessState *state)
440465
{
441466
int docstring = _PyAST_GetDocString(stmts) != NULL;
442467
if (docstring && (state->optimize >= 2)) {
443468
/* remove the docstring */
444-
if (!stmt_seq_remove_item(stmts, 0)) {
469+
if (!remove_docstring(stmts, 0, ctx_)) {
445470
return 0;
446471
}
447472
docstring = 0;

0 commit comments

Comments
 (0)