Skip to content
24 changes: 18 additions & 6 deletions Lib/test/test_clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,8 @@ def converter_init(self):
self.expect_failure(raw, err)

def test_clone_mismatch(self):
err = "'kind' of function and cloned function don't match!"
err = ("'kind' of function and cloned function don't match: "
"STATIC_METHOD != CLASS_METHOD")
block = """
/*[clinic input]
module m
Expand Down Expand Up @@ -574,7 +575,7 @@ class C "void *" ""
C.__init__ = C.meth
[clinic start generated code]*/
"""
err = "'__init__' must be a normal method; got 'FunctionKind.CLASS_METHOD'!"
err = "'__init__' must be an instance method; got 'FunctionKind.CLASS_METHOD'"
self.expect_failure(block, err, lineno=8)

def test_validate_cloned_new(self):
Expand Down Expand Up @@ -1906,7 +1907,9 @@ def test_parameters_not_permitted_after_slash_for_now(self):
self.expect_failure(block, err)

def test_parameters_no_more_than_one_vararg(self):
err = "Too many var args"
err = ("Cannot specify multiple vararg parameters: "
"'vararg2' is a vararg, but "
"'vararg1' was already provided")
block = """
module foo
foo.bar
Expand Down Expand Up @@ -2039,6 +2042,15 @@ def test_legacy_converters_non_string_constant_annotation(self):
with self.subTest(block=block):
self.expect_failure(block, err, lineno=2)

def test_str_converter_invalid_format_unit(self):
block = """
module foo
foo.bar
a: str(encoding='foo', zeroes=True, accept={})
"""
err = "unsupported combination of str converter arguments"
self.expect_failure(block, err, lineno=2)

def test_other_bizarre_things_in_annotations_fail(self):
err = "Annotations must be either a name, a function call, or a string"
dataset = (
Expand Down Expand Up @@ -2120,7 +2132,7 @@ class Foo "" ""
self.parse_function(block)

def test_new_must_be_a_class_method(self):
err = "'__new__' must be a class method!"
err = "'__new__' must be a class method; got 'FunctionKind.CALLABLE'"
block = """
module foo
class Foo "" ""
Expand All @@ -2129,7 +2141,7 @@ class Foo "" ""
self.expect_failure(block, err, lineno=2)

def test_init_must_be_a_normal_method(self):
err_template = "'__init__' must be a normal method; got 'FunctionKind.{}'!"
err_template = "'__init__' must be an instance method; got 'FunctionKind.{}'"
annotations = {
"@classmethod": "CLASS_METHOD",
"@staticmethod": "STATIC_METHOD",
Expand Down Expand Up @@ -2317,7 +2329,7 @@ def test_non_ascii_character_in_docstring(self):
self.assertEqual(stdout.getvalue(), expected)

def test_illegal_c_identifier(self):
err = "Illegal C identifier: 17a"
err = "Expected a legal C identifier; got '17a'"
block = """
module test
test.fn
Expand Down
50 changes: 36 additions & 14 deletions Tools/clinic/clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3965,14 +3965,27 @@ class buffer: pass
class rwbuffer: pass
class robuffer: pass

StrConverterKeyType = tuple[frozenset[type[object]], bool, bool]
@dc.dataclass
class StrConverterKey:
accept: frozenset[type[object]]
encoding: bool
zeroes: bool

def __hash__(self) -> int:
return hash((self.accept, self.encoding, self.zeroes))

def __str__(self) -> str:
accept = "{" + ", ".join([tp.__name__ for tp in self.accept]) + "}"
encoding = 'encodingname' if self.encoding else None
zeroes = self.zeroes
return f"{accept=!r}, {encoding=!r}, {zeroes=!r}"

def str_converter_key(
types: TypeSet, encoding: bool | str | None, zeroes: bool
) -> StrConverterKeyType:
return (frozenset(types), bool(encoding), bool(zeroes))
) -> StrConverterKey:
return StrConverterKey(frozenset(types), bool(encoding), bool(zeroes))

str_converter_argument_map: dict[StrConverterKeyType, str] = {}
str_converter_argument_map: dict[StrConverterKey, str] = {}

class str_converter(CConverter):
type = 'const char *'
Expand All @@ -3990,7 +4003,10 @@ def converter_init(
key = str_converter_key(accept, encoding, zeroes)
format_unit = str_converter_argument_map.get(key)
if not format_unit:
fail("str_converter: illegal combination of arguments", key)
allowed = "\n".join([str(k) for k in str_converter_argument_map.keys()])
fail("unsupported combination of str converter arguments: "
f"{accept=!r}, {encoding=!r}, {zeroes=!r}; "
f"allowed combinations are:\n\n{allowed}")

self.format_unit = format_unit
self.length = bool(zeroes)
Expand Down Expand Up @@ -4931,12 +4947,14 @@ def directive_preserve(self) -> None:

def at_classmethod(self) -> None:
if self.kind is not CALLABLE:
fail("Can't set @classmethod, function is not a normal callable")
fail("Can't set @classmethod, "
f"function is not a normal callable; got: {self.kind!r}")
self.kind = CLASS_METHOD

def at_critical_section(self, *args: str) -> None:
if len(args) > 2:
fail("Up to 2 critical section variables are supported")
fail("Only 2 critical section variables are supported; "
f"{len(args)} were given")
self.target_critical_section.extend(args)
self.critical_section = True

Expand All @@ -4960,7 +4978,8 @@ def at_setter(self) -> None:

def at_staticmethod(self) -> None:
if self.kind is not CALLABLE:
fail("Can't set @staticmethod, function is not a normal callable")
fail("Can't set @staticmethod, "
f"function is not a normal callable; got: {self.kind!r}")
self.kind = STATIC_METHOD

def at_coexist(self) -> None:
Expand Down Expand Up @@ -5074,9 +5093,9 @@ def normalize_function_kind(self, fullname: str) -> None:
if name in unsupported_special_methods:
fail(f"{name!r} is a special method and cannot be converted to Argument Clinic!")
if name == '__init__' and (self.kind is not CALLABLE or not cls):
fail(f"{name!r} must be a normal method; got '{self.kind}'!")
fail(f"{name!r} must be an instance method; got '{self.kind}'!")
if name == '__new__' and (self.kind is not CLASS_METHOD or not cls):
fail("'__new__' must be a class method!")
fail(f"'__new__' must be a class method; got '{self.kind}'")
if self.kind in {GETTER, SETTER} and not cls:
fail("@getter and @setter must be methods")

Expand Down Expand Up @@ -5150,8 +5169,8 @@ def parse_cloned_function(self, names: FunctionNames, existing: str) -> None:
# Future enhancement: allow custom return converters
overrides["return_converter"] = CReturnConverter()
else:
fail("'kind' of function and cloned function don't match! "
"(@classmethod/@staticmethod/@coexist)")
fail("'kind' of function and cloned function don't match: "
f"{self.kind.name} != {existing_function.kind.name}")
function = existing_function.copy(**overrides)
self.function = function
self.block.signatures.append(function)
Expand Down Expand Up @@ -5424,10 +5443,13 @@ def parse_parameter(self, line: str) -> None:
f"invalid parameter declaration (**kwargs?): {line!r}")

if function_args.vararg:
if any(p.is_vararg() for p in self.function.parameters.values()):
fail("Too many var args")
is_vararg = True
parameter = function_args.vararg
for p in self.function.parameters.values():
if p.is_vararg():
fail("Cannot specify multiple vararg parameters: "
f"{parameter.arg!r} is a vararg, but "
f"{p.name!r} was already provided as a vararg")
else:
is_vararg = False
parameter = function_args.args[0]
Expand Down
2 changes: 1 addition & 1 deletion Tools/clinic/libclinic/identifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def is_legal_py_identifier(identifier: str) -> bool:
def ensure_legal_c_identifier(identifier: str) -> str:
# For now, just complain if what we're given isn't legal.
if not is_legal_c_identifier(identifier):
raise ClinicError(f"Illegal C identifier: {identifier}")
raise ClinicError(f"Expected a legal C identifier; got {identifier!r}")
# But if we picked a C keyword, pick something else.
if identifier in _c_keywords:
return identifier + "_value"
Expand Down