Skip to content

Commit 2d9bd81

Browse files
committed
gh-155228: Check the length of parameter descriptions in Argument Clinic
The docstring line width check ran before the {parameters} block was substituted into the docstring, so parameter descriptions of any length were accepted without a warning. This was a leftover of gh-150285, and was easiest to miss for cloned functions, which inherit the parameter descriptions of the function they clone. Measure the rendered parameter description lines as part of the docstring body, taking the indentation of the {parameters} marker into account, and rewrap the 30 functions in the tree whose descriptions exceeded their limit. The width warnings now also report the file they come from. The issue lists 24 functions; the check reports 30. Three of the listed functions (bytes.decode, bytearray.decode and winreg.ConnectRegistry) are 1 to 5 characters under their limit on this commit and are left unchanged.
1 parent c5d4946 commit 2d9bd81

36 files changed

Lines changed: 380 additions & 196 deletions

Lib/test/test_clinic.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,18 @@ def checkDocstring(self, fn, expected):
10661066
self.assertEqual(dedent(expected).strip(),
10671067
fn.docstring.strip())
10681068

1069+
def parse_warnings(self, block):
1070+
"""Parse a block and return what Argument Clinic warned about."""
1071+
with support.captured_stdout() as stdout:
1072+
self.parse(block)
1073+
return stdout.getvalue()
1074+
1075+
def too_long_warning(self, full_name, max_width):
1076+
"""The warning emitted for a too long docstring body line."""
1077+
return (f"Warning in file {'clinic_tests'!r}:\n"
1078+
f"Docstring lines for {full_name!r} are too long!\n"
1079+
f"Lines should be no longer than {max_width} characters.\n\n")
1080+
10691081
def test_trivial(self):
10701082
parser = DSLParser(_make_clinic())
10711083
block = Block("""
@@ -2499,6 +2511,126 @@ def test_docstring_explicit_params_placement(self):
24992511
(Note the added newline)
25002512
""")
25012513

2514+
def test_long_summary_line(self):
2515+
# The summary line must fit in 72 characters for a function.
2516+
block = f"""
2517+
module m
2518+
m.f
2519+
{'x' * 73}
2520+
"""
2521+
err = ("Summary line for 'm.f' is too long!\n"
2522+
"The summary line must be no longer than 72 characters.")
2523+
self.expect_failure(block, err)
2524+
2525+
def test_long_summary_line_permitted(self):
2526+
block = f"""
2527+
@permit_long_summary
2528+
module m
2529+
m.f
2530+
{'x' * 73}
2531+
"""
2532+
self.assertEqual(self.parse_warnings(block), "")
2533+
2534+
def test_long_parameter_docstring(self):
2535+
# gh-155228: a parameter description is part of the docstring body,
2536+
# even though it is only substituted for the {parameters} marker
2537+
# after the width check. Descriptions are indented by 4 spaces.
2538+
expected = self.too_long_warning('m.f', 72)
2539+
for length, warning in (68, ""), (69, expected):
2540+
with self.subTest(length=length):
2541+
block = f"""
2542+
module m
2543+
m.f
2544+
a: int
2545+
{'x' * length}
2546+
The summary line.
2547+
"""
2548+
self.assertEqual(self.parse_warnings(block), warning)
2549+
2550+
def test_long_parameter_docstring_method(self):
2551+
# Methods get 4 characters less than functions.
2552+
expected = self.too_long_warning('m.C.f', 68)
2553+
for length, warning in (64, ""), (65, expected):
2554+
with self.subTest(length=length):
2555+
block = f"""
2556+
module m
2557+
class m.C "void *" ""
2558+
m.C.f
2559+
a: int
2560+
{'x' * length}
2561+
The summary line.
2562+
"""
2563+
self.assertEqual(self.parse_warnings(block), warning)
2564+
2565+
def test_long_parameter_docstring_indented_marker(self):
2566+
# linear_format() indents the substituted parameters by the
2567+
# indentation of the {parameters} marker line, which counts
2568+
# towards the width as well.
2569+
expected = self.too_long_warning('m.f', 72)
2570+
for length, warning in (66, ""), (67, expected):
2571+
with self.subTest(length=length):
2572+
block = f"""
2573+
module m
2574+
m.f
2575+
a: int
2576+
{'x' * length}
2577+
The summary line.
2578+
2579+
{{parameters}}
2580+
"""
2581+
self.assertEqual(self.parse_warnings(block), warning)
2582+
2583+
def test_long_parameter_docstring_permitted(self):
2584+
block = f"""
2585+
@permit_long_docstring_body
2586+
module m
2587+
m.f
2588+
a: int
2589+
{'x' * 69}
2590+
The summary line.
2591+
"""
2592+
self.assertEqual(self.parse_warnings(block), "")
2593+
2594+
def test_permit_long_docstring_body_not_needed(self):
2595+
block = f"""
2596+
@permit_long_docstring_body
2597+
module m
2598+
m.f
2599+
a: int
2600+
{'x' * 68}
2601+
The summary line.
2602+
"""
2603+
expected = (
2604+
f"Warning in file {'clinic_tests'!r}:\n"
2605+
"Remove the @permit_long_docstring_body decorator from 'm.f'!\n\n\n"
2606+
)
2607+
self.assertEqual(self.parse_warnings(block), expected)
2608+
2609+
def test_long_parameter_docstring_cloned(self):
2610+
# gh-155228: a clone inherits the parameter descriptions of the
2611+
# function it clones, so it must be reported as well.
2612+
# The clone lives in its own block, as it does in the source tree.
2613+
blocks = (
2614+
f"""
2615+
module m
2616+
m.f
2617+
a: int
2618+
{'x' * 69}
2619+
The summary line.
2620+
""",
2621+
"""
2622+
m.g = m.f
2623+
The other summary line.
2624+
""",
2625+
)
2626+
parser = DSLParser(_make_clinic())
2627+
with support.captured_stdout() as stdout:
2628+
for text in blocks:
2629+
parser.parse(Block(text))
2630+
expected = "".join(self.too_long_warning(f'm.{name}', 72)
2631+
for name in ("f", "g"))
2632+
self.assertEqual(stdout.getvalue(), expected)
2633+
25022634
def test_indent_stack_no_tabs(self):
25032635
block = """
25042636
module foo

Modules/_lzmamodule.c

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,20 +1186,20 @@ _lzma.LZMADecompressor.__new__
11861186
11871187
format: int(c_default="FORMAT_AUTO") = FORMAT_AUTO
11881188
Specifies the container format of the input stream. If this is
1189-
FORMAT_AUTO (the default), the decompressor will automatically detect
1190-
whether the input is FORMAT_XZ or FORMAT_ALONE. Streams created with
1191-
FORMAT_RAW cannot be autodetected.
1189+
FORMAT_AUTO (the default), the decompressor will automatically
1190+
detect whether the input is FORMAT_XZ or FORMAT_ALONE. Streams
1191+
created with FORMAT_RAW cannot be autodetected.
11921192
11931193
memlimit: object = None
1194-
Limit the amount of memory used by the decompressor. This will cause
1195-
decompression to fail if the input cannot be decompressed within the
1196-
given limit.
1194+
Limit the amount of memory used by the decompressor. This will
1195+
cause decompression to fail if the input cannot be decompressed
1196+
within the given limit.
11971197
11981198
filters: object = None
1199-
A custom filter chain. This argument is required for FORMAT_RAW, and
1200-
not accepted with any other format. When provided, this should be a
1201-
sequence of dicts, each indicating the ID and options for a single
1202-
filter.
1199+
A custom filter chain. This argument is required for FORMAT_RAW,
1200+
and not accepted with any other format. When provided, this
1201+
should be a sequence of dicts, each indicating the ID and options
1202+
for a single filter.
12031203
12041204
Create a decompressor object for decompressing data incrementally.
12051205
@@ -1209,7 +1209,7 @@ For one-shot decompression, use the decompress() function instead.
12091209
static PyObject *
12101210
_lzma_LZMADecompressor_impl(PyTypeObject *type, int format,
12111211
PyObject *memlimit, PyObject *filters)
1212-
/*[clinic end generated code: output=2d46d5e70f10bc7f input=ca40cd1cb1202b0d]*/
1212+
/*[clinic end generated code: output=2d46d5e70f10bc7f input=a9b1c4db9f5acb69]*/
12131213
{
12141214
Decompressor *self;
12151215
const uint32_t decoder_flags = LZMA_TELL_ANY_CHECK | LZMA_TELL_NO_CHECK;

Modules/_sqlite/clinic/connection.c.h

Lines changed: 5 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_sqlite/connection.c

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2539,7 +2539,8 @@ is_int_config(const int op)
25392539
_sqlite3.Connection.setconfig as setconfig
25402540
25412541
op: int
2542-
The configuration verb; one of the sqlite3.SQLITE_DBCONFIG codes.
2542+
The configuration verb;
2543+
one of the sqlite3.SQLITE_DBCONFIG codes.
25432544
enable: bool = True
25442545
/
25452546
@@ -2548,7 +2549,7 @@ Set a boolean connection configuration option.
25482549

25492550
static PyObject *
25502551
setconfig_impl(pysqlite_Connection *self, int op, int enable)
2551-
/*[clinic end generated code: output=c60b13e618aff873 input=a10f1539c2d7da6b]*/
2552+
/*[clinic end generated code: output=c60b13e618aff873 input=8f00e4c0d499abcb]*/
25522553
{
25532554
if (!pysqlite_check_thread(self) || !pysqlite_check_connection(self)) {
25542555
return NULL;
@@ -2574,15 +2575,16 @@ setconfig_impl(pysqlite_Connection *self, int op, int enable)
25742575
_sqlite3.Connection.getconfig as getconfig -> bool
25752576
25762577
op: int
2577-
The configuration verb; one of the sqlite3.SQLITE_DBCONFIG codes.
2578+
The configuration verb;
2579+
one of the sqlite3.SQLITE_DBCONFIG codes.
25782580
/
25792581
25802582
Query a boolean connection configuration option.
25812583
[clinic start generated code]*/
25822584

25832585
static int
25842586
getconfig_impl(pysqlite_Connection *self, int op)
2585-
/*[clinic end generated code: output=25ac05044c7b78a3 input=b0526d7e432e3f2f]*/
2587+
/*[clinic end generated code: output=25ac05044c7b78a3 input=835b01bdd9069c02]*/
25862588
{
25872589
if (!pysqlite_check_thread(self) || !pysqlite_check_connection(self)) {
25882590
return -1;

Modules/_sre/clinic/sre.c.h

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_sre/sre.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1862,16 +1862,16 @@ _sre.template
18621862
18631863
pattern: object
18641864
template: object(subclass_of="&PyList_Type")
1865-
A list containing interleaved literal strings (str or bytes) and group
1866-
indices (int), as returned by re._parser.parse_template():
1865+
A list containing interleaved literal strings (str or bytes) and
1866+
group indices (int), as returned by re._parser.parse_template():
18671867
[literal1, group1, ..., literalN, groupN]
18681868
/
18691869
18701870
[clinic start generated code]*/
18711871

18721872
static PyObject *
18731873
_sre_template_impl(PyObject *module, PyObject *pattern, PyObject *template)
1874-
/*[clinic end generated code: output=d51290e596ebca86 input=af55380b27f02942]*/
1874+
/*[clinic end generated code: output=d51290e596ebca86 input=e015cbc1c71d0d20]*/
18751875
{
18761876
/* template is a list containing interleaved literal strings (str or bytes)
18771877
* and group indices (int), as returned by _parser.parse_template:

Modules/_winapi.c

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3012,8 +3012,9 @@ _winapi_CopyFile2_impl(PyObject *module, LPCWSTR existing_file_name,
30123012
_winapi.RegisterEventSource -> HANDLE
30133013
30143014
unc_server_name: LPCWSTR(accept={str, NoneType})
3015-
The UNC name of the server on which the event source should be registered.
3016-
If None, registers the event source on the local computer.
3015+
The UNC name of the server on which the event source should be
3016+
registered. If None, registers the event source on the local
3017+
computer.
30173018
source_name: LPCWSTR
30183019
The name of the event source to register.
30193020
/
@@ -3024,7 +3025,7 @@ Retrieves a registered handle to the specified event log.
30243025
static HANDLE
30253026
_winapi_RegisterEventSource_impl(PyObject *module, LPCWSTR unc_server_name,
30263027
LPCWSTR source_name)
3027-
/*[clinic end generated code: output=e376c8950a89ae8f input=9d01059ac2156d0c]*/
3028+
/*[clinic end generated code: output=e376c8950a89ae8f input=ca9cb7b8959582dd]*/
30283029
{
30293030
HANDLE handle;
30303031

Modules/_zstd/_zstdmodule.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,8 @@ _zstd.finalize_dict
342342
dict_size: Py_ssize_t
343343
The size of the dictionary.
344344
compression_level: int
345-
Optimize for a specific Zstandard compression level, 0 means default.
345+
Optimize for a specific Zstandard compression level,
346+
0 means default.
346347
/
347348
348349
Finalize a Zstandard dictionary.
@@ -353,7 +354,7 @@ _zstd_finalize_dict_impl(PyObject *module, PyBytesObject *custom_dict_bytes,
353354
PyBytesObject *samples_bytes,
354355
PyObject *samples_sizes, Py_ssize_t dict_size,
355356
int compression_level)
356-
/*[clinic end generated code: output=f91821ba5ae85bda input=3c7e2480aa08fb56]*/
357+
/*[clinic end generated code: output=f91821ba5ae85bda input=954d58d6f20c85c2]*/
357358
{
358359
Py_ssize_t chunks_number;
359360
size_t *chunk_sizes = NULL;

Modules/_zstd/clinic/_zstdmodule.c.h

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_zstd/clinic/compressor.c.h

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)