[3.12] gh-106368: Clean up Argument Clinic tests (#106373) (#106379)

(cherry picked from commit 3ee8dac7a1)
This commit is contained in:
Erlend E. Aasland 2023-07-04 00:27:34 +02:00 committed by GitHub
parent 67127ca8e2
commit 38fe0e7c2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -3,7 +3,8 @@
# Licensed to the PSF under a contributor agreement. # Licensed to the PSF under a contributor agreement.
from test import support, test_tools from test import support, test_tools
from test.support import import_helper, os_helper from test.support import os_helper
from textwrap import dedent
from unittest import TestCase from unittest import TestCase
import collections import collections
import inspect import inspect
@ -286,22 +287,38 @@ xyz
class ClinicParserTest(TestCase): class ClinicParserTest(TestCase):
def checkDocstring(self, fn, expected):
self.assertTrue(hasattr(fn, "docstring"))
self.assertEqual(fn.docstring.strip(),
dedent(expected).strip())
def test_trivial(self): def test_trivial(self):
parser = DSLParser(FakeClinic()) parser = DSLParser(FakeClinic())
block = clinic.Block("module os\nos.access") block = clinic.Block("""
module os
os.access
""")
parser.parse(block) parser.parse(block)
module, function = block.signatures module, function = block.signatures
self.assertEqual("access", function.name) self.assertEqual("access", function.name)
self.assertEqual("os", module.name) self.assertEqual("os", module.name)
def test_ignore_line(self): def test_ignore_line(self):
block = self.parse("#\nmodule os\nos.access") block = self.parse(dedent("""
#
module os
os.access
"""))
module, function = block.signatures module, function = block.signatures
self.assertEqual("access", function.name) self.assertEqual("access", function.name)
self.assertEqual("os", module.name) self.assertEqual("os", module.name)
def test_param(self): def test_param(self):
function = self.parse_function("module os\nos.access\n path: int") function = self.parse_function("""
module os
os.access
path: int
""")
self.assertEqual("access", function.name) self.assertEqual("access", function.name)
self.assertEqual(2, len(function.parameters)) self.assertEqual(2, len(function.parameters))
p = function.parameters['path'] p = function.parameters['path']
@ -309,47 +326,79 @@ class ClinicParserTest(TestCase):
self.assertIsInstance(p.converter, clinic.int_converter) self.assertIsInstance(p.converter, clinic.int_converter)
def test_param_default(self): def test_param_default(self):
function = self.parse_function("module os\nos.access\n follow_symlinks: bool = True") function = self.parse_function("""
module os
os.access
follow_symlinks: bool = True
""")
p = function.parameters['follow_symlinks'] p = function.parameters['follow_symlinks']
self.assertEqual(True, p.default) self.assertEqual(True, p.default)
def test_param_with_continuations(self): def test_param_with_continuations(self):
function = self.parse_function("module os\nos.access\n follow_symlinks: \\\n bool \\\n =\\\n True") function = self.parse_function(r"""
module os
os.access
follow_symlinks: \
bool \
= \
True
""")
p = function.parameters['follow_symlinks'] p = function.parameters['follow_symlinks']
self.assertEqual(True, p.default) self.assertEqual(True, p.default)
def test_param_default_expression(self): def test_param_default_expression(self):
function = self.parse_function("module os\nos.access\n follow_symlinks: int(c_default='MAXSIZE') = sys.maxsize") function = self.parse_function("""
module os
os.access
follow_symlinks: int(c_default='MAXSIZE') = sys.maxsize
""")
p = function.parameters['follow_symlinks'] p = function.parameters['follow_symlinks']
self.assertEqual(sys.maxsize, p.default) self.assertEqual(sys.maxsize, p.default)
self.assertEqual("MAXSIZE", p.converter.c_default) self.assertEqual("MAXSIZE", p.converter.c_default)
s = self.parse_function_should_fail("module os\nos.access\n follow_symlinks: int = sys.maxsize") expected_msg = (
self.assertEqual(s, "Error on line 0:\nWhen you specify a named constant ('sys.maxsize') as your default value,\nyou MUST specify a valid c_default.\n") "Error on line 0:\n"
"When you specify a named constant ('sys.maxsize') as your default value,\n"
"you MUST specify a valid c_default.\n"
)
out = self.parse_function_should_fail("""
module os
os.access
follow_symlinks: int = sys.maxsize
""")
self.assertEqual(out, expected_msg)
def test_param_no_docstring(self): def test_param_no_docstring(self):
function = self.parse_function(""" function = self.parse_function("""
module os module os
os.access os.access
follow_symlinks: bool = True follow_symlinks: bool = True
something_else: str = ''""") something_else: str = ''
""")
p = function.parameters['follow_symlinks'] p = function.parameters['follow_symlinks']
self.assertEqual(3, len(function.parameters)) self.assertEqual(3, len(function.parameters))
self.assertIsInstance(function.parameters['something_else'].converter, clinic.str_converter) conv = function.parameters['something_else'].converter
self.assertIsInstance(conv, clinic.str_converter)
def test_param_default_parameters_out_of_order(self): def test_param_default_parameters_out_of_order(self):
s = self.parse_function_should_fail(""" expected_msg = (
"Error on line 0:\n"
"Can't have a parameter without a default ('something_else')\n"
"after a parameter with a default!\n"
)
out = self.parse_function_should_fail("""
module os module os
os.access os.access
follow_symlinks: bool = True follow_symlinks: bool = True
something_else: str""") something_else: str""")
self.assertEqual(s, """Error on line 0: self.assertEqual(out, expected_msg)
Can't have a parameter without a default ('something_else')
after a parameter with a default!
""")
def disabled_test_converter_arguments(self): def disabled_test_converter_arguments(self):
function = self.parse_function("module os\nos.access\n path: path_t(allow_fd=1)") function = self.parse_function("""
module os
os.access
path: path_t(allow_fd=1)
""")
p = function.parameters['path'] p = function.parameters['path']
self.assertEqual(1, p.converter.args['allow_fd']) self.assertEqual(1, p.converter.args['allow_fd'])
@ -361,8 +410,9 @@ os.stat as os_stat_fn
path: str path: str
Path to be examined Path to be examined
Perform a stat system call on the given path.""") Perform a stat system call on the given path.
self.assertEqual(""" """)
self.checkDocstring(function, """
stat($module, /, path) stat($module, /, path)
-- --
@ -370,10 +420,10 @@ Perform a stat system call on the given path.
path path
Path to be examined Path to be examined
""".strip(), function.docstring) """)
def test_explicit_parameters_in_docstring(self): def test_explicit_parameters_in_docstring(self):
function = self.parse_function(""" function = self.parse_function(dedent("""
module foo module foo
foo.bar foo.bar
x: int x: int
@ -383,8 +433,8 @@ foo.bar
This is the documentation for foo. This is the documentation for foo.
Okay, we're done here. Okay, we're done here.
""") """))
self.assertEqual(""" self.checkDocstring(function, """
bar($module, /, x, y) bar($module, /, x, y)
-- --
@ -394,33 +444,56 @@ This is the documentation for foo.
Documentation for x. Documentation for x.
Okay, we're done here. Okay, we're done here.
""".strip(), function.docstring) """)
def test_parser_regression_special_character_in_parameter_column_of_docstring_first_line(self): def test_parser_regression_special_character_in_parameter_column_of_docstring_first_line(self):
function = self.parse_function(""" function = self.parse_function(dedent("""
module os module os
os.stat os.stat
path: str path: str
This/used to break Clinic! This/used to break Clinic!
"""))
self.checkDocstring(function, """
stat($module, /, path)
--
This/used to break Clinic!
""") """)
self.assertEqual("stat($module, /, path)\n--\n\nThis/used to break Clinic!", function.docstring)
def test_c_name(self): def test_c_name(self):
function = self.parse_function("module os\nos.stat as os_stat_fn") function = self.parse_function("""
module os
os.stat as os_stat_fn
""")
self.assertEqual("os_stat_fn", function.c_basename) self.assertEqual("os_stat_fn", function.c_basename)
def test_return_converter(self): def test_return_converter(self):
function = self.parse_function("module os\nos.stat -> int") function = self.parse_function("""
module os
os.stat -> int
""")
self.assertIsInstance(function.return_converter, clinic.int_return_converter) self.assertIsInstance(function.return_converter, clinic.int_return_converter)
def test_star(self): def test_star(self):
function = self.parse_function("module os\nos.access\n *\n follow_symlinks: bool = True") function = self.parse_function("""
module os
os.access
*
follow_symlinks: bool = True
""")
p = function.parameters['follow_symlinks'] p = function.parameters['follow_symlinks']
self.assertEqual(inspect.Parameter.KEYWORD_ONLY, p.kind) self.assertEqual(inspect.Parameter.KEYWORD_ONLY, p.kind)
self.assertEqual(0, p.group) self.assertEqual(0, p.group)
def test_group(self): def test_group(self):
function = self.parse_function("module window\nwindow.border\n [\n ls : int\n ]\n /\n") function = self.parse_function("""
module window
window.border
[
ls: int
]
/
""")
p = function.parameters['ls'] p = function.parameters['ls']
self.assertEqual(1, p.group) self.assertEqual(1, p.group)
@ -442,15 +515,17 @@ curses.addch
] ]
/ /
""") """)
for name, group in ( dataset = (
('y', -1), ('x', -1), ('y', -1), ('x', -1),
('ch', 0), ('ch', 0),
('attr', 1), ('attr', 1),
): )
for name, group in dataset:
with self.subTest(name=name, group=group):
p = function.parameters[name] p = function.parameters[name]
self.assertEqual(p.group, group) self.assertEqual(p.group, group)
self.assertEqual(p.kind, inspect.Parameter.POSITIONAL_ONLY) self.assertEqual(p.kind, inspect.Parameter.POSITIONAL_ONLY)
self.assertEqual(function.docstring.strip(), """ self.checkDocstring(function, """
addch([y, x,] ch, [attr]) addch([y, x,] ch, [attr])
@ -462,7 +537,7 @@ addch([y, x,] ch, [attr])
Character to add. Character to add.
attr attr
Attributes for the character. Attributes for the character.
""".strip()) """)
def test_nested_groups(self): def test_nested_groups(self):
function = self.parse_function(""" function = self.parse_function("""
@ -500,18 +575,20 @@ curses.imaginary
] ]
/ /
""") """)
for name, group in ( dataset = (
('y1', -2), ('y2', -2), ('y1', -2), ('y2', -2),
('x1', -1), ('x2', -1), ('x1', -1), ('x2', -1),
('ch', 0), ('ch', 0),
('attr1', 1), ('attr2', 1), ('attr3', 1), ('attr1', 1), ('attr2', 1), ('attr3', 1),
('attr4', 2), ('attr5', 2), ('attr6', 2), ('attr4', 2), ('attr5', 2), ('attr6', 2),
): )
for name, group in dataset:
with self.subTest(name=name, group=group):
p = function.parameters[name] p = function.parameters[name]
self.assertEqual(p.group, group) self.assertEqual(p.group, group)
self.assertEqual(p.kind, inspect.Parameter.POSITIONAL_ONLY) self.assertEqual(p.kind, inspect.Parameter.POSITIONAL_ONLY)
self.assertEqual(function.docstring.strip(), """ self.checkDocstring(function, """
imaginary([[y1, y2,] x1, x2,] ch, [attr1, attr2, attr3, [attr4, attr5, imaginary([[y1, y2,] x1, x2,] ch, [attr1, attr2, attr3, [attr4, attr5,
attr6]]) attr6]])
@ -538,7 +615,7 @@ imaginary([[y1, y2,] x1, x2,] ch, [attr1, attr2, attr3, [attr4, attr5,
Attributes for the character. Attributes for the character.
attr6 attr6
Attributes for the character. Attributes for the character.
""".strip()) """)
def parse_function_should_fail(self, s): def parse_function_should_fail(self, s):
with support.captured_stdout() as stdout: with support.captured_stdout() as stdout:
@ -547,7 +624,12 @@ imaginary([[y1, y2,] x1, x2,] ch, [attr1, attr2, attr3, [attr4, attr5,
return stdout.getvalue() return stdout.getvalue()
def test_disallowed_grouping__two_top_groups_on_left(self): def test_disallowed_grouping__two_top_groups_on_left(self):
s = self.parse_function_should_fail(""" expected_msg = (
'Error on line 0:\n'
'Function two_top_groups_on_left has an unsupported group '
'configuration. (Unexpected state 2.b)\n'
)
out = self.parse_function_should_fail("""
module foo module foo
foo.two_top_groups_on_left foo.two_top_groups_on_left
[ [
@ -558,9 +640,7 @@ foo.two_top_groups_on_left
] ]
param: int param: int
""") """)
self.assertEqual(s, self.assertEqual(out, expected_msg)
('Error on line 0:\n'
'Function two_top_groups_on_left has an unsupported group configuration. (Unexpected state 2.b)\n'))
def test_disallowed_grouping__two_top_groups_on_right(self): def test_disallowed_grouping__two_top_groups_on_right(self):
self.parse_function_should_fail(""" self.parse_function_should_fail("""
@ -645,6 +725,7 @@ foo.Bar.__init__
Docstring Docstring
""", signatures_in_block=3, function_index=2) """, signatures_in_block=3, function_index=2)
# self is not in the signature # self is not in the signature
self.assertEqual("Bar()\n--\n\nDocstring", function.docstring) self.assertEqual("Bar()\n--\n\nDocstring", function.docstring)
# but it *is* a parameter # but it *is* a parameter
@ -732,13 +813,17 @@ foo.bar
""") """)
def test_parameters_no_more_than_one_vararg(self): def test_parameters_no_more_than_one_vararg(self):
s = self.parse_function_should_fail(""" expected_msg = (
"Error on line 0:\n"
"Too many var args\n"
)
out = self.parse_function_should_fail("""
module foo module foo
foo.bar foo.bar
*vararg1: object *vararg1: object
*vararg2: object *vararg2: object
""") """)
self.assertEqual(s, "Error on line 0:\nToo many var args\n") self.assertEqual(out, expected_msg)
def test_function_not_at_column_0(self): def test_function_not_at_column_0(self):
function = self.parse_function(""" function = self.parse_function("""
@ -750,7 +835,7 @@ foo.bar
y: str y: str
Not at column 0! Not at column 0!
""") """)
self.assertEqual(""" self.checkDocstring(function, """
bar($module, /, x, *, y) bar($module, /, x, *, y)
-- --
@ -758,7 +843,7 @@ Not at column 0!
x x
Nested docstring here, goeth. Nested docstring here, goeth.
""".strip(), function.docstring) """)
def test_directive(self): def test_directive(self):
c = FakeClinic() c = FakeClinic()
@ -772,46 +857,39 @@ Not at column 0!
def test_legacy_converters(self): def test_legacy_converters(self):
block = self.parse('module os\nos.access\n path: "s"') block = self.parse('module os\nos.access\n path: "s"')
module, function = block.signatures module, function = block.signatures
self.assertIsInstance((function.parameters['path']).converter, clinic.str_converter) conv = (function.parameters['path']).converter
self.assertIsInstance(conv, clinic.str_converter)
def test_legacy_converters_non_string_constant_annotation(self): def test_legacy_converters_non_string_constant_annotation(self):
expected_failure_message = """\ expected_failure_message = (
Error on line 0: "Error on line 0:\n"
Annotations must be either a name, a function call, or a string. "Annotations must be either a name, a function call, or a string.\n"
""" )
dataset = (
s = self.parse_function_should_fail('module os\nos.access\n path: 42') 'module os\nos.access\n path: 42',
self.assertEqual(s, expected_failure_message) 'module os\nos.access\n path: 42.42',
'module os\nos.access\n path: 42j',
s = self.parse_function_should_fail('module os\nos.access\n path: 42.42') 'module os\nos.access\n path: b"42"',
self.assertEqual(s, expected_failure_message) )
for block in dataset:
s = self.parse_function_should_fail('module os\nos.access\n path: 42j') with self.subTest(block=block):
self.assertEqual(s, expected_failure_message) out = self.parse_function_should_fail(block)
self.assertEqual(out, expected_failure_message)
s = self.parse_function_should_fail('module os\nos.access\n path: b"42"')
self.assertEqual(s, expected_failure_message)
def test_other_bizarre_things_in_annotations_fail(self): def test_other_bizarre_things_in_annotations_fail(self):
expected_failure_message = """\ expected_failure_message = (
Error on line 0: "Error on line 0:\n"
Annotations must be either a name, a function call, or a string. "Annotations must be either a name, a function call, or a string.\n"
"""
s = self.parse_function_should_fail(
'module os\nos.access\n path: {"some": "dictionary"}'
) )
self.assertEqual(s, expected_failure_message) dataset = (
'module os\nos.access\n path: {"some": "dictionary"}',
s = self.parse_function_should_fail( 'module os\nos.access\n path: ["list", "of", "strings"]',
'module os\nos.access\n path: ["list", "of", "strings"]' 'module os\nos.access\n path: (x for x in range(42))',
) )
self.assertEqual(s, expected_failure_message) for block in dataset:
with self.subTest(block=block):
s = self.parse_function_should_fail( out = self.parse_function_should_fail(block)
'module os\nos.access\n path: (x for x in range(42))' self.assertEqual(out, expected_failure_message)
)
self.assertEqual(s, expected_failure_message)
def test_kwarg_splats_disallowed_in_function_call_annotations(self): def test_kwarg_splats_disallowed_in_function_call_annotations(self):
expected_error_msg = ( expected_error_msg = (
@ -945,10 +1023,16 @@ Annotations must be either a name, a function call, or a string.
self.assertEqual(repr(clinic.NULL), '<Null>') self.assertEqual(repr(clinic.NULL), '<Null>')
# test that fail fails # test that fail fails
expected = (
'Error in file "clown.txt" on line 69:\n'
'The igloos are melting!\n'
)
with support.captured_stdout() as stdout: with support.captured_stdout() as stdout:
with self.assertRaises(SystemExit): with self.assertRaises(SystemExit):
clinic.fail('The igloos are melting!', filename='clown.txt', line_number=69) clinic.fail('The igloos are melting!',
self.assertEqual(stdout.getvalue(), 'Error in file "clown.txt" on line 69:\nThe igloos are melting!\n') filename='clown.txt', line_number=69)
actual = stdout.getvalue()
self.assertEqual(actual, expected)
class ClinicExternalTest(TestCase): class ClinicExternalTest(TestCase):