8000 gh-115077: Make some Argument Clinic error messages more helpful by erlend-aasland · Pull Request #115078 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

gh-115077: Make some Argument Clinic error messages more helpful #115078

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Prev Previous commit
Next Next commit
Pull in main
  • Loading branch information
erlend-aasland committed Mar 2, 2024
commit b0277b2cdb4871b7132bb36d4b6bb202cca458b5
5 changes: 3 additions & 2 deletions Lib/test/test_clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1907,8 +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 = ("Cannot specify multiple varargs; "
"'*vararg1' was already provided as parameter 1")
err = ("Cannot specify multiple vararg parameters: "
"'vararg2' is a vararg, but "
"'vararg1' was already provided")
block = """
module foo
foo.bar
Expand Down
143 changes: 22 additions & 121 deletions Tools/clinic/clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,75 +138,6 @@ def fail(
warn_or_fail(*args, filename=filename, line_number=line_number, fail=True)


is_legal_c_identifier = re.compile('^[A-Za-z_][A-Za-z0-9_]*$').match

def is_legal_py_identifier(s: str) -> bool:
return all(is_legal_c_identifier(field) for field in s.split('.'))

# identifiers that are okay in Python but aren't a good idea in C.
# so if they're used Argument Clinic will add "_value" to the end
# of the name in C.
c_keywords = set("""
asm auto break case char const continue default do double
else enum extern float for goto if inline int long
register return short signed sizeof static struct switch
typedef typeof union unsigned void volatile while
""".strip().split())

def ensure_legal_c_identifier(s: str) -> str:
# for now, just complain if what we're given isn't legal
if not is_legal_c_identifier(s):
fail(f"Expected a legal C identifier; got {s!r}")
# but if we picked a C keyword, pick something else
if s in c_keywords:
return s + "_value"
return s


def linear_format(s: str, **kwargs: str) -> str:
"""
Perform str.format-like substitution, except:
* The strings substituted must be on lines by
themselves. (This line is the "source line".)
* If the substitution text is empty, the source line
is removed in the output.
* If the field is not recognized, the original line
is passed unmodified through to the output.
* If the substitution text is not empty:
* Each line of the substituted text is indented
by the indent of the source line.
* A newline will be added to the end.
"""
lines = []
for line in s.split('\n'):
indent, curly, trailing = line.partition('{')
if not curly:
lines.extend([line, "\n"])
continue

name, curly, trailing = trailing.partition('}')
if not curly or name not in kwargs:
lines.extend([line, "\n"])
continue

if trailing:
fail(f"Text found after {{{name}}} block marker! "
"It must be on a line by itself.")
if indent.strip():
fail(f"Non-whitespace characters found before {{{name}}} block marker! "
"It must be on a line by itself.")

value = kwargs[name]
if not value:
continue

stripped = [line.rstrip() for line in value.split("\n")]
value = textwrap.indent("\n".join(stripped), indent)
lines.extend([value, "\n"])

return "".join(lines[:-1])


class CRenderData:
def __init__(self) -> None:

Expand Down Expand Up @@ -828,10 +759,6 @@ def output_templates(
if not p.is_optional():
min_kw_only = i - max_pos
elif p.is_vararg():
if vararg != self.NO_VARARG:
assert isinstance(vararg, int)
fail("Cannot specify multiple varargs; "
f"'*{parameters[vararg].name}' was already provided as parameter {vararg+1}")
pseudo_args += 1
vararg = i - 1
else:
Expand Down Expand Up @@ -5242,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 @@ -5276,47 +5203,8 @@ def state_modulename_name(self, line: str) -> None:
existing = existing.strip()
if libclinic.is_legal_py_identifier(existing):
# we're cloning!
fields = [x.strip() for x in existing.split('.')]
function_name = fields.pop()
module, cls = self.clinic._module_and_class(fields)

for existing_function in (cls or module).functions:
if existing_function.name == function_name:
break
else:
print(f"{cls=}, {module=}, {existing=}", file=sys.stderr)
print(f"{(cls or module).functions=}", file=sys.stderr)
fail(f"Couldn't find existing function {existing!r}!")

fields = [x.strip() for x in full_name.split('.')]
function_name = fields.pop()
module, cls = self.clinic._module_and_class(fields)

self.update_function_kind(full_name)
overrides: dict[str, Any] = {
"name": function_name,
"full_name": full_name,
"module": module,
"cls": cls,
"c_basename": c_basename,
"docstring": "",
}
if not (existing_function.kind is self.kind and
existing_function.coexist == self.coexist):
# Allow __new__ or __init__ methods.
if existing_function.kind.new_or_init:
overrides["kind"] = self.kind
# Future enhancement: allow custom return converters
overrides["return_converter"] = CReturnConverter()
else:
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)
(cls or module).functions.append(function)
self.next(self.state_function_docstring)
return
names = self.parse_function_names(before)
return self.parse_cloned_function(names, existing)

line, _, returns = line.partition('->')
returns = returns.strip()
Expand All @@ -5327,9 +5215,19 @@ def state_modulename_name(self, line: str) -> None:
function_name = fields.pop()
module, cls = self.clinic._module_and_class(fields)

if self.kind in {GETTER, SETTER}:
if not cls:
fail("@getter and @setter must be methods, not functions")
func = Function(
name=function_name,
full_name=full_name,
module=module,
cls=cls,
c_basename=c_basename,
return_converter=return_converter,
kind=self.kind,
coexist=self.coexist,
critical_section=self.critical_section,
target_critical_section=self.target_critical_section
)
self.add_function(func)

self.next(self.state_parameters_start)

Expand Down Expand Up @@ -5545,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
You are viewing a condensed version of this merge commit. You can view the full changes here.
0