From b3d4da38307b74bd956e6affd0caffde218fb743 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 30 Jan 2024 14:41:24 +0100 Subject: [PATCH] gh-113317: Remove Argument Clinic global variable 'clinic' * Add Clinic.warn() and Clinic.fail() methods. * Add filename parameter to BlockParser constructor. * Add BlockParser.fail() method. * Add DSLParser.fail() method. * Pass lineno parameter to DSLParser.format_docstring(): more accurate error reporting. * Convert a few DSLParser static methods to regular method to be able to call self.fail(). * Language.render() clinic parameter is now required: None is no longer accepted. * Remove warn() function. * Remove global variable 'clinic'. fail() and warn() methods get filename and line number from the instance on which the method is called, rather than relying on a global variable 'clinic'. --- Lib/test/test_clinic.py | 4 +- Tools/clinic/clinic.py | 353 +++++++++++++++++++++------------------- 2 files changed, 186 insertions(+), 171 deletions(-) diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index 7323bdd801f4be..c2d1b3feea17df 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -2390,7 +2390,7 @@ def test_state_func_docstring_no_summary(self): docstring1 docstring2 """ - self.expect_failure(block, err, lineno=0) + self.expect_failure(block, err, lineno=3) def test_state_func_docstring_only_one_param_template(self): err = "You may not specify {parameters} more than once in a docstring!" @@ -2404,7 +2404,7 @@ def test_state_func_docstring_only_one_param_template(self): these are the params again: {parameters} """ - self.expect_failure(block, err, lineno=0) + self.expect_failure(block, err, lineno=7) class ClinicExternalTest(TestCase): diff --git a/Tools/clinic/clinic.py b/Tools/clinic/clinic.py index 770878a3f8d2c7..04cf8430542d76 100755 --- a/Tools/clinic/clinic.py +++ b/Tools/clinic/clinic.py @@ -116,11 +116,6 @@ def warn_or_fail( line_number: int | None = None, ) -> None: joined = " ".join([str(a) for a in args]) - if clinic: - if filename is None: - filename = clinic.filename - if getattr(clinic, 'block_parser', None) and (line_number is None): - line_number = clinic.block_parser.line_number error = ClinicError(joined, filename=filename, lineno=line_number) if fail: raise error @@ -128,13 +123,6 @@ def warn_or_fail( print(error.report(warn_only=True)) -def warn( - *args: object, - filename: str | None = None, - line_number: int | None = None, -) -> None: - return warn_or_fail(*args, filename=filename, line_number=line_number, fail=False) - def fail( *args: object, filename: str | None = None, @@ -282,7 +270,7 @@ def __init__(self, filename: str) -> None: @abc.abstractmethod def render( self, - clinic: Clinic | None, + clinic: Clinic, signatures: Iterable[Module | Class | Function] ) -> str: ... @@ -635,14 +623,16 @@ def parse_line(self, line: str) -> None: def render( self, - clinic: Clinic | None, + clinic: Clinic, signatures: Iterable[Module | Class | Function] ) -> str: function = None for o in signatures: if isinstance(o, Function): - if function: - fail("You may specify at most one function per block.\nFound a block containing at least two:\n\t" + repr(function) + " and " + repr(o)) + if function is not None: + clinic.fail("You may specify at most one function per block.\n" + "Found a block containing at least two:\n" + "\t" + repr(function) + " and " + repr(o)) function = o return self.render_function(clinic, function) @@ -834,7 +824,7 @@ def output_templates( min_kw_only = i - max_pos elif p.is_vararg(): if vararg != self.NO_VARARG: - fail("Too many var args") + clinic.fail("Too many var args") pseudo_args += 1 vararg = i - 1 else: @@ -877,7 +867,7 @@ def output_templates( docstring_prototype = docstring_definition = '' elif f.kind is SETTER: if f.docstring: - fail("docstrings are only supported for @getter, not @setter") + clinic.fail("docstrings are only supported for @getter, not @setter") return_value_declaration = "int {return_value};" methoddef_define = self.SETTERDEF_PROTOTYPE_DEFINE docstring_prototype = docstring_definition = '' @@ -930,12 +920,12 @@ def parser_body( (any(p.is_optional() for p in parameters) and any(p.is_keyword_only() and not p.is_optional() for p in parameters)) or any(c.broken_limited_capi for c in converters)): - warn(f"Function {f.full_name} cannot use limited C API") + clinic.warn(f"Function {f.full_name} cannot use limited C API") limited_capi = False parsearg: str | None if f.kind in {GETTER, SETTER} and parameters: - fail(f"@{f.kind.name.lower()} method cannot define parameters") + clinic.fail(f"@{f.kind.name.lower()} method cannot define parameters") if not parameters: parser_code: list[str] | None @@ -1652,8 +1642,8 @@ def render_function( c.render(p, data) if has_option_groups and (not positional): - fail("You cannot use optional groups ('[' and ']') " - "unless all parameters are positional-only ('/').") + clinic.fail("You cannot use optional groups ('[' and ']') " + "unless all parameters are positional-only ('/').") # HACK # when we're METH_O, but have a custom return converter, @@ -1855,7 +1845,8 @@ def __init__( input: str, language: Language, *, - verify: bool = True + verify: bool = True, + filename: str | None = None, ) -> None: """ "input" should be a str object @@ -1875,11 +1866,17 @@ def __init__( whole_line=False) self.start_re = libclinic.create_regex(before, after) self.verify = verify + self.filename = filename self.last_checksum_re: re.Pattern[str] | None = None self.last_dsl_name: str | None = None self.dsl_name: str | None = None self.first_block = True + def fail(self, *args: object) -> NoReturn: + warn_or_fail(*args, + filename=self.filename, line_number=self.line_number, + fail=True) + def __iter__(self) -> BlockParser: return self @@ -1937,12 +1934,12 @@ def is_stop_line(line: str) -> bool: if line.startswith(stop_line): remainder = line.removeprefix(stop_line) if remainder and not remainder.isspace(): - fail(f"Garbage after stop line: {remainder!r}") + self.fail(f"Garbage after stop line: {remainder!r}") return True else: # gh-92256: don't allow incorrectly formatted stop lines if line.lstrip().startswith(stop_line): - fail(f"Whitespace is not allowed before the stop line: {line!r}") + self.fail(f"Whitespace is not allowed before the stop line: {line!r}") return False # consume body of program @@ -1987,7 +1984,7 @@ def is_stop_line(line: str) -> bool: for field in shlex.split(arguments): name, equals, value = field.partition('=') if not equals: - fail(f"Mangled Argument Clinic marker line: {line!r}") + self.fail(f"Mangled Argument Clinic marker line: {line!r}") d[name.strip()] = value.strip() if self.verify: @@ -1998,10 +1995,10 @@ def is_stop_line(line: str) -> bool: computed = libclinic.compute_checksum(output, len(checksum)) if checksum != computed: - fail("Checksum mismatch! " - f"Expected {checksum!r}, computed {computed!r}. " - "Suggested fix: remove all generated code including " - "the end marker, or use the '-f' option.") + self.fail("Checksum mismatch! " + f"Expected {checksum!r}, computed {computed!r}. " + "Suggested fix: remove all generated code including " + "the end marker, or use the '-f' option.") else: # put back output output_lines = output.splitlines(keepends=True) @@ -2222,7 +2219,6 @@ def __init__(self, clinic: Clinic) -> None: ... def parse(self, block: Block) -> None: ... -clinic: Clinic | None = None class Clinic: presets_text = """ @@ -2347,8 +2343,18 @@ def __init__( assert name in self.destination_buffers preset[name] = buffer - global clinic - clinic = self + def warn(self, *args: object) -> None: + warn_or_fail(*args, + filename=self.filename, + line_number=self.block_parser.line_number, + fail=False) + + def fail(self, *args: object, line_number: int | None = None) -> NoReturn: + if line_number is None: + line_number = self.block_parser.line_number + warn_or_fail(*args, + filename=self.filename, line_number=line_number, + fail=True) def add_include(self, name: str, reason: str, *, condition: str | None = None) -> None: @@ -2394,7 +2400,9 @@ def get_destination_buffer( def parse(self, input: str) -> str: printer = self.printer - self.block_parser = BlockParser(input, self.language, verify=self.verify) + self.block_parser = BlockParser(input, self.language, + verify=self.verify, + filename=self.filename) for block in self.block_parser: dsl_name = block.dsl_name if dsl_name: @@ -2418,7 +2426,7 @@ def parse(self, input: str) -> str: if destination.type == 'buffer': block.input = "dump " + name + "\n" - warn("Destination buffer " + repr(name) + " not empty at end of file, emptying.") + self.warn("Destination buffer " + repr(name) + " not empty at end of file, emptying.") printer.write("\n") printer.print_block(block, limited_capi=self.limited_capi, @@ -4877,6 +4885,9 @@ def __init__(self, clinic: Clinic) -> None: self.reset() + def fail(self, *args: object, line_number: int | None = None) -> NoReturn: + self.clinic.fail(*args, line_number=line_number) + def reset(self) -> None: self.function = None self.state = self.state_dsl_start @@ -4899,10 +4910,10 @@ def directive_module(self, name: str) -> None: fields = name.split('.')[:-1] module, cls = self.clinic._module_and_class(fields) if cls: - fail("Can't nest a module inside a class!") + self.fail("Can't nest a module inside a class!") if name in module.modules: - fail(f"Already defined module {name!r}!") + self.fail(f"Already defined module {name!r}!") m = Module(name, module) module.modules[name] = m @@ -4920,7 +4931,7 @@ def directive_class( parent = cls or module if name in parent.classes: - fail(f"Already defined class {name!r}!") + self.fail(f"Already defined class {name!r}!") c = Class(name, module, cls, typedef, type_object) parent.classes[name] = c @@ -4928,7 +4939,7 @@ def directive_class( def directive_set(self, name: str, value: str) -> None: if name not in ("line_prefix", "line_suffix"): - fail(f"unknown variable {name!r}") + self.fail(f"unknown variable {name!r}") value = value.format_map({ 'block comment start': '/*', @@ -4949,7 +4960,7 @@ def directive_destination( case "clear": self.clinic.get_destination(name).clear() case _: - fail(f"unknown destination command {command!r}") + self.fail(f"unknown destination command {command!r}") def directive_output( @@ -4962,7 +4973,7 @@ def directive_output( if command_or_name == "preset": preset = self.clinic.presets.get(destination) if not preset: - fail(f"Unknown preset {destination!r}!") + self.fail(f"Unknown preset {destination!r}!") fd.update(preset) return @@ -4972,7 +4983,7 @@ def directive_output( if command_or_name == "pop": if not self.clinic.destination_buffers_stack: - fail("Can't 'output pop', stack is empty!") + self.fail("Can't 'output pop', stack is empty!") previous_fd = self.clinic.destination_buffers_stack.pop() fd.update(previous_fd) return @@ -4993,9 +5004,10 @@ def directive_output( if command_or_name not in fd: allowed = ["preset", "push", "pop", "print", "everything"] allowed.extend(fd) - fail(f"Invalid command or destination name {command_or_name!r}. " - "Must be one of:\n -", - "\n - ".join([repr(word) for word in allowed])) + self.fail( + f"Invalid command or destination name {command_or_name!r}. " + "Must be one of:\n -", + "\n - ".join([repr(word) for word in allowed])) fd[command_or_name] = d def directive_dump(self, name: str) -> None: @@ -5007,51 +5019,51 @@ def directive_printout(self, *args: str) -> None: def directive_preserve(self) -> None: if self.preserve_output: - fail("Can't have 'preserve' twice in one block!") + self.fail("Can't have 'preserve' twice in one block!") self.preserve_output = True def at_classmethod(self) -> None: if self.kind is not CALLABLE: - fail("Can't set @classmethod, function is not a normal callable") + self.fail("Can't set @classmethod, function is not a normal callable") 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") + self.fail("Up to 2 critical section variables are supported") self.target_critical_section.extend(args) self.critical_section = True def at_getter(self) -> None: match self.kind: case FunctionKind.GETTER: - fail("Cannot apply @getter twice to the same function!") + self.fail("Cannot apply @getter twice to the same function!") case FunctionKind.SETTER: - fail("Cannot apply both @getter and @setter to the same function!") + self.fail("Cannot apply both @getter and @setter to the same function!") case _: self.kind = FunctionKind.GETTER def at_setter(self) -> None: match self.kind: case FunctionKind.SETTER: - fail("Cannot apply @setter twice to the same function!") + self.fail("Cannot apply @setter twice to the same function!") case FunctionKind.GETTER: - fail("Cannot apply both @getter and @setter to the same function!") + self.fail("Cannot apply both @getter and @setter to the same function!") case _: self.kind = FunctionKind.SETTER def at_staticmethod(self) -> None: if self.kind is not CALLABLE: - fail("Can't set @staticmethod, function is not a normal callable") + self.fail("Can't set @staticmethod, function is not a normal callable") self.kind = STATIC_METHOD def at_coexist(self) -> None: if self.coexist: - fail("Called @coexist twice!") + self.fail("Called @coexist twice!") self.coexist = True def at_text_signature(self, text_signature: str) -> None: if self.forced_text_signature: - fail("Called @text_signature twice!") + self.fail("Called @text_signature twice!") self.forced_text_signature = text_signature def parse(self, block: Block) -> None: @@ -5063,8 +5075,8 @@ def parse(self, block: Block) -> None: lines = block.input.split('\n') for line_number, line in enumerate(lines, self.clinic.block_parser.block_start_line_number): if '\t' in line: - fail(f'Tab characters are illegal in the Clinic DSL: {line!r}', - line_number=block_start) + self.fail(f'Tab characters are illegal in the Clinic DSL: {line!r}', + line_number=block_start) try: self.state(line) except ClinicError as exc: @@ -5076,7 +5088,8 @@ def parse(self, block: Block) -> None: if self.preserve_output: if block.output: - fail("'preserve' only works for blocks that don't produce any output!") + self.fail("'preserve' only works for blocks that don't produce any output!", + line_number=block_start) block.output = self.saved_output def in_docstring(self) -> bool: @@ -5119,27 +5132,26 @@ def state_dsl_start(self, line: str) -> None: try: directive(*fields[1:]) except TypeError as e: - fail(str(e)) + self.fail(str(e)) return self.next(self.state_modulename_name, line) - @staticmethod - def parse_function_names(line: str) -> FunctionNames: + def parse_function_names(self, line: str) -> FunctionNames: left, as_, right = line.partition(' as ') full_name = left.strip() c_basename = right.strip() if as_ and not c_basename: - fail("No C basename provided after 'as' keyword") + self.fail("No C basename provided after 'as' keyword") if not c_basename: fields = full_name.split(".") if fields[-1] == '__new__': fields.pop() c_basename = "_".join(fields) if not is_legal_py_identifier(full_name): - fail(f"Illegal function name: {full_name!r}") + self.fail(f"Illegal function name: {full_name!r}") if not is_legal_c_identifier(c_basename): - fail(f"Illegal C basename: {c_basename!r}") + self.fail(f"Illegal C basename: {c_basename!r}") return FunctionNames(full_name=full_name, c_basename=c_basename) def update_function_kind(self, fullname: str) -> None: @@ -5147,18 +5159,18 @@ def update_function_kind(self, fullname: str) -> None: name = fields.pop() _, cls = self.clinic._module_and_class(fields) if name in unsupported_special_methods: - fail(f"{name!r} is a special method and cannot be converted to Argument Clinic!") + self.fail(f"{name!r} is a special method and cannot be converted to Argument Clinic!") if name == '__new__': if (self.kind is CLASS_METHOD) and cls: self.kind = METHOD_NEW else: - fail("'__new__' must be a class method!") + self.fail("'__new__' must be a class method!") elif name == '__init__': if (self.kind is CALLABLE) and cls: self.kind = METHOD_INIT else: - fail( + self.fail( "'__init__' must be a normal method; " f"got '{self.kind}'!" ) @@ -5200,7 +5212,7 @@ def state_modulename_name(self, line: str) -> None: 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}!") + self.fail(f"Couldn't find existing function {existing!r}!") fields = [x.strip() for x in full_name.split('.')] function_name = fields.pop() @@ -5223,8 +5235,8 @@ def state_modulename_name(self, line: 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)") + self.fail("'kind' of function and cloned function don't match! " + "(@classmethod/@staticmethod/@coexist)") function = existing_function.copy(**overrides) self.function = function self.block.signatures.append(function) @@ -5239,23 +5251,23 @@ def state_modulename_name(self, line: str) -> None: return_converter = None if returns: if self.kind in {GETTER, SETTER}: - fail(f"@{self.kind.name.lower()} method cannot define a return type") + self.fail(f"@{self.kind.name.lower()} method cannot define a return type") ast_input = f"def x() -> {returns}: pass" try: module_node = ast.parse(ast_input) except SyntaxError: - fail(f"Badly formed annotation for {full_name!r}: {returns!r}") + self.fail(f"Badly formed annotation for {full_name!r}: {returns!r}") function_node = module_node.body[0] assert isinstance(function_node, ast.FunctionDef) try: name, legacy, kwargs = self.parse_converter(function_node.returns) if legacy: - fail(f"Legacy converter {name!r} not allowed as a return converter") + self.fail(f"Legacy converter {name!r} not allowed as a return converter") if name not in return_converters: - fail(f"No available return converter called {name!r}") + self.fail(f"No available return converter called {name!r}") return_converter = return_converters[name](**kwargs) except ValueError: - fail(f"Badly formed annotation for {full_name!r}: {returns!r}") + self.fail(f"Badly formed annotation for {full_name!r}: {returns!r}") fields = [x.strip() for x in full_name.split('.')] function_name = fields.pop() @@ -5263,7 +5275,7 @@ def state_modulename_name(self, line: str) -> None: if self.kind in {GETTER, SETTER}: if not cls: - fail("@getter and @setter must be methods") + self.fail("@getter and @setter must be methods") self.update_function_kind(full_name) if self.kind is METHOD_INIT and not return_converter: @@ -5427,7 +5439,8 @@ def parse_parameter(self, line: str) -> None: case ParamState.GROUP_AFTER | ParamState.OPTIONAL: pass case st: - fail(f"Function {self.function.name} has an unsupported group configuration. (Unexpected state {st}.a)") + self.fail(f"Function {self.function.name} has an unsupported " + f"group configuration. (Unexpected state {st}.a)") # handle "as" for parameters too c_name = None @@ -5437,7 +5450,7 @@ def parse_parameter(self, line: str) -> None: if ' ' not in name: fields = trailing.strip().split(' ') if not fields: - fail("Invalid 'as' clause!") + self.fail("Invalid 'as' clause!") c_name = fields[0] if c_name.endswith(':'): name += ':' @@ -5466,22 +5479,22 @@ def parse_parameter(self, line: str) -> None: except SyntaxError: pass if not module: - fail(f"Function {self.function.name!r} has an invalid parameter declaration:\n\t", - repr(line)) + self.fail(f"Function {self.function.name!r} has an invalid parameter declaration:\n\t", + repr(line)) function = module.body[0] assert isinstance(function, ast.FunctionDef) function_args = function.args if len(function_args.args) > 1: - fail(f"Function {self.function.name!r} has an " - f"invalid parameter declaration (comma?): {line!r}") + self.fail(f"Function {self.function.name!r} has an " + f"invalid parameter declaration (comma?): {line!r}") if function_args.defaults or function_args.kw_defaults: - fail(f"Function {self.function.name!r} has an " - f"invalid parameter declaration (default value?): {line!r}") + self.fail(f"Function {self.function.name!r} has an " + f"invalid parameter declaration (default value?): {line!r}") if function_args.kwarg: - fail(f"Function {self.function.name!r} has an " - f"invalid parameter declaration (**kwargs?): {line!r}") + self.fail(f"Function {self.function.name!r} has an " + f"invalid parameter declaration (**kwargs?): {line!r}") if function_args.vararg: is_vararg = True @@ -5496,18 +5509,18 @@ def parse_parameter(self, line: str) -> None: value: object if not default: if self.parameter_state is ParamState.OPTIONAL: - fail(f"Can't have a parameter without a default ({parameter_name!r}) " - "after a parameter with a default!") + self.fail(f"Can't have a parameter without a default ({parameter_name!r}) " + "after a parameter with a default!") if is_vararg: value = NULL kwargs.setdefault('c_default', "NULL") else: value = unspecified if 'py_default' in kwargs: - fail("You can't specify py_default without specifying a default value!") + self.fail("You can't specify py_default without specifying a default value!") else: if is_vararg: - fail("Vararg can't take a default value!") + self.fail("Vararg can't take a default value!") if self.parameter_state is ParamState.REQUIRED: self.parameter_state = ParamState.OPTIONAL @@ -5553,13 +5566,13 @@ def bad_node(self, node: ast.AST) -> None: except NameError: pass # probably a named constant except Exception as e: - fail("Malformed expression given as default value " - f"{default!r} caused {e!r}") + self.fail("Malformed expression given as default value " + f"{default!r} caused {e!r}") else: if value is unspecified: - fail("'unspecified' is not a legal default value!") + self.fail("'unspecified' is not a legal default value!") if bad: - fail(f"Unsupported expression as default value: {default!r}") + self.fail(f"Unsupported expression as default value: {default!r}") assignment = module.body[0] assert isinstance(assignment, ast.Assign) @@ -5577,10 +5590,10 @@ def bad_node(self, node: ast.AST) -> None: )): c_default = kwargs.get("c_default") if not (isinstance(c_default, str) and c_default): - fail(f"When you specify an expression ({default!r}) " - f"as your default value, " - f"you MUST specify a valid c_default.", - ast.dump(expr)) + self.fail(f"When you specify an expression ({default!r}) " + f"as your default value, " + f"you MUST specify a valid c_default.", + ast.dump(expr)) py_default = default value = unknown elif isinstance(expr, ast.Attribute): @@ -5590,16 +5603,16 @@ def bad_node(self, node: ast.AST) -> None: a.append(n.attr) n = n.value if not isinstance(n, ast.Name): - fail(f"Unsupported default value {default!r} " - "(looked like a Python constant)") + self.fail(f"Unsupported default value {default!r} " + "(looked like a Python constant)") a.append(n.id) py_default = ".".join(reversed(a)) c_default = kwargs.get("c_default") if not (isinstance(c_default, str) and c_default): - fail(f"When you specify a named constant ({py_default!r}) " - "as your default value, " - "you MUST specify a valid c_default.") + self.fail(f"When you specify a named constant ({py_default!r}) " + "as your default value, " + "you MUST specify a valid c_default.") try: value = eval(py_default) @@ -5616,15 +5629,15 @@ def bad_node(self, node: ast.AST) -> None: c_default = py_default except SyntaxError as e: - fail(f"Syntax error: {e.text!r}") + self.fail(f"Syntax error: {e.text!r}") except (ValueError, AttributeError): value = unknown c_default = kwargs.get("c_default") py_default = default if not (isinstance(c_default, str) and c_default): - fail("When you specify a named constant " - f"({py_default!r}) as your default value, " - "you MUST specify a valid c_default.") + self.fail("When you specify a named constant " + f"({py_default!r}) as your default value, " + "you MUST specify a valid c_default.") kwargs.setdefault('c_default', c_default) kwargs.setdefault('py_default', py_default) @@ -5632,7 +5645,7 @@ def bad_node(self, node: ast.AST) -> None: dict = legacy_converters if legacy else converters legacy_str = "legacy " if legacy else "" if name not in dict: - fail(f'{name!r} is not a valid {legacy_str}converter') + self.fail(f'{name!r} is not a valid {legacy_str}converter') # if you use a c_name for the parameter, we just give that name to the converter # but the parameter object gets the python name converter = dict[name](c_name or parameter_name, parameter_name, self.function, value, **kwargs) @@ -5648,31 +5661,31 @@ def bad_node(self, node: ast.AST) -> None: if isinstance(converter, self_converter): if len(self.function.parameters) == 1: if self.parameter_state is not ParamState.REQUIRED: - fail("A 'self' parameter cannot be marked optional.") + self.fail("A 'self' parameter cannot be marked optional.") if value is not unspecified: - fail("A 'self' parameter cannot have a default value.") + self.fail("A 'self' parameter cannot have a default value.") if self.group: - fail("A 'self' parameter cannot be in an optional group.") + self.fail("A 'self' parameter cannot be in an optional group.") kind = inspect.Parameter.POSITIONAL_ONLY self.parameter_state = ParamState.START self.function.parameters.clear() else: - fail("A 'self' parameter, if specified, must be the " - "very first thing in the parameter block.") + self.fail("A 'self' parameter, if specified, must be the " + "very first thing in the parameter block.") if isinstance(converter, defining_class_converter): _lp = len(self.function.parameters) if _lp == 1: if self.parameter_state is not ParamState.REQUIRED: - fail("A 'defining_class' parameter cannot be marked optional.") + self.fail("A 'defining_class' parameter cannot be marked optional.") if value is not unspecified: - fail("A 'defining_class' parameter cannot have a default value.") + self.fail("A 'defining_class' parameter cannot have a default value.") if self.group: - fail("A 'defining_class' parameter cannot be in an optional group.") + self.fail("A 'defining_class' parameter cannot be in an optional group.") else: - fail("A 'defining_class' parameter, if specified, must either " - "be the first thing in the parameter block, or come just " - "after 'self'.") + self.fail("A 'defining_class' parameter, if specified, must either " + "be the first thing in the parameter block, or come just " + "after 'self'.") p = Parameter(parameter_name, kind, function=self.function, @@ -5681,16 +5694,15 @@ def bad_node(self, node: ast.AST) -> None: names = [k.name for k in self.function.parameters.values()] if parameter_name in names[1:]: - fail(f"You can't have two parameters named {parameter_name!r}!") + self.fail(f"You can't have two parameters named {parameter_name!r}!") elif names and parameter_name == names[0] and c_name is None: - fail(f"Parameter {parameter_name!r} requires a custom C name") + self.fail(f"Parameter {parameter_name!r} requires a custom C name") key = f"{parameter_name}_as_{c_name}" if c_name else parameter_name self.function.parameters[key] = p - @staticmethod def parse_converter( - annotation: ast.expr | None + self, annotation: ast.expr | None ) -> tuple[str, bool, ConverterArgs]: match annotation: case ast.Constant(value=str() as value): @@ -5702,11 +5714,11 @@ def parse_converter( kwargs: ConverterArgs = {} for node in annotation.keywords: if not isinstance(node.arg, str): - fail("Cannot use a kwarg splat in a function-call annotation") + self.fail("Cannot use a kwarg splat in a function-call annotation") kwargs[node.arg] = eval_ast_expr(node.value, symbols) return name, False, kwargs case _: - fail( + self.fail( "Annotations must be either a name, a function call, or a string." ) @@ -5718,7 +5730,7 @@ def parse_version(self, thenceforth: str) -> VersionTuple: major, minor = thenceforth.split(".") return int(major), int(minor) except ValueError: - fail( + self.fail( f"Function {self.function.name!r}: expected format '[from major.minor]' " f"where 'major' and 'minor' are integers; got {thenceforth!r}" ) @@ -5731,21 +5743,21 @@ def parse_star(self, function: Function, version: VersionTuple | None) -> None: """ if version is None: if self.keyword_only: - fail(f"Function {function.name!r} uses '*' more than once.") + self.fail(f"Function {function.name!r} uses '*' more than once.") self.check_previous_star() self.check_remaining_star() self.keyword_only = True else: if self.keyword_only: - fail(f"Function {function.name!r}: '* [from ...]' must precede '*'") + self.fail(f"Function {function.name!r}: '* [from ...]' must precede '*'") if self.deprecated_positional: if self.deprecated_positional == version: - fail(f"Function {function.name!r} uses '* [from " - f"{version[0]}.{version[1]}]' more than once.") + self.fail(f"Function {function.name!r} uses '* [from " + f"{version[0]}.{version[1]}]' more than once.") if self.deprecated_positional < version: - fail(f"Function {function.name!r}: '* [from " - f"{version[0]}.{version[1]}]' must precede '* [from " - f"{self.deprecated_positional[0]}.{self.deprecated_positional[1]}]'") + self.fail(f"Function {function.name!r}: '* [from " + f"{version[0]}.{version[1]}]' must precede '* [from " + f"{self.deprecated_positional[0]}.{self.deprecated_positional[1]}]'") self.deprecated_positional = version def parse_opening_square_bracket(self, function: Function) -> None: @@ -5756,19 +5768,19 @@ def parse_opening_square_bracket(self, function: Function) -> None: case ParamState.REQUIRED | ParamState.GROUP_AFTER: self.parameter_state = ParamState.GROUP_AFTER case st: - fail(f"Function {function.name!r} " - f"has an unsupported group configuration. " - f"(Unexpected state {st}.b)") + self.fail(f"Function {function.name!r} " + f"has an unsupported group configuration. " + f"(Unexpected state {st}.b)") self.group += 1 function.docstring_only = True def parse_closing_square_bracket(self, function: Function) -> None: """Parse closing parameter group symbol ']'.""" if not self.group: - fail(f"Function {function.name!r} has a ']' without a matching '['.") + self.fail(f"Function {function.name!r} has a ']' without a matching '['.") if not any(p.group == self.group for p in function.parameters.values()): - fail(f"Function {function.name!r} has an empty group. " - "All groups must contain at least one parameter.") + self.fail(f"Function {function.name!r} has an empty group. " + "All groups must contain at least one parameter.") self.group -= 1 match self.parameter_state: case ParamState.LEFT_SQUARE_BEFORE | ParamState.GROUP_BEFORE: @@ -5776,9 +5788,9 @@ def parse_closing_square_bracket(self, function: Function) -> None: case ParamState.GROUP_AFTER | ParamState.RIGHT_SQUARE_AFTER: self.parameter_state = ParamState.RIGHT_SQUARE_AFTER case st: - fail(f"Function {function.name!r} " - f"has an unsupported group configuration. " - f"(Unexpected state {st}.c)") + self.fail(f"Function {function.name!r} " + f"has an unsupported group configuration. " + f"(Unexpected state {st}.c)") def parse_slash(self, function: Function, version: VersionTuple | None) -> None: """Parse positional-only parameter marker '/'. @@ -5788,26 +5800,26 @@ def parse_slash(self, function: Function, version: VersionTuple | None) -> None: """ if version is None: if self.deprecated_keyword: - fail(f"Function {function.name!r}: '/' must precede '/ [from ...]'") + self.fail(f"Function {function.name!r}: '/' must precede '/ [from ...]'") if self.deprecated_positional: - fail(f"Function {function.name!r}: '/' must precede '* [from ...]'") + self.fail(f"Function {function.name!r}: '/' must precede '* [from ...]'") if self.keyword_only: - fail(f"Function {function.name!r}: '/' must precede '*'") + self.fail(f"Function {function.name!r}: '/' must precede '*'") if self.positional_only: - fail(f"Function {function.name!r} uses '/' more than once.") + self.fail(f"Function {function.name!r} uses '/' more than once.") else: if self.deprecated_keyword: if self.deprecated_keyword == version: - fail(f"Function {function.name!r} uses '/ [from " - f"{version[0]}.{version[1]}]' more than once.") + self.fail(f"Function {function.name!r} uses '/ [from " + f"{version[0]}.{version[1]}]' more than once.") if self.deprecated_keyword > version: - fail(f"Function {function.name!r}: '/ [from " - f"{version[0]}.{version[1]}]' must precede '/ [from " - f"{self.deprecated_keyword[0]}.{self.deprecated_keyword[1]}]'") + self.fail(f"Function {function.name!r}: '/ [from " + f"{version[0]}.{version[1]}]' must precede '/ [from " + f"{self.deprecated_keyword[0]}.{self.deprecated_keyword[1]}]'") if self.deprecated_positional: - fail(f"Function {function.name!r}: '/ [from ...]' must precede '* [from ...]'") + self.fail(f"Function {function.name!r}: '/ [from ...]' must precede '* [from ...]'") if self.keyword_only: - fail(f"Function {function.name!r}: '/ [from ...]' must precede '*'") + self.fail(f"Function {function.name!r}: '/ [from ...]' must precede '*'") self.positional_only = True self.deprecated_keyword = version if version is not None: @@ -5816,8 +5828,8 @@ def parse_slash(self, function: Function, version: VersionTuple | None) -> None: found = p.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD break if not found: - fail(f"Function {function.name!r} specifies '/ [from ...]' " - f"without preceding parameters.") + self.fail(f"Function {function.name!r} specifies '/ [from ...]' " + f"without preceding parameters.") # REQUIRED and OPTIONAL are allowed here, that allows positional-only # without option groups to work (and have default values!) allowed = { @@ -5827,8 +5839,8 @@ def parse_slash(self, function: Function, version: VersionTuple | None) -> None: ParamState.GROUP_BEFORE, } if (self.parameter_state not in allowed) or self.group: - fail(f"Function {function.name!r} has an unsupported group configuration. " - f"(Unexpected state {self.parameter_state}.d)") + self.fail(f"Function {function.name!r} has an unsupported group configuration. " + f"(Unexpected state {self.parameter_state}.d)") # fixup preceding parameters for p in function.parameters.values(): if p.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD: @@ -5852,8 +5864,7 @@ def docstring_append(self, obj: Function | Parameter, line: str) -> None: # so you may be able to remove this restriction. matches = re.finditer(r'[^\x00-\x7F]', line) if offending := ", ".join([repr(m[0]) for m in matches]): - warn("Non-ascii characters are not allowed in docstrings:", - offending) + self.clinic.warn("Non-ascii characters are not allowed in docstrings:", offending) docstring = obj.docstring if docstring: @@ -5888,7 +5899,7 @@ def state_function_docstring(self, line: str) -> None: assert self.function is not None if self.group: - fail(f"Function {self.function.name!r} has a ']' without a matching '['.") + self.fail(f"Function {self.function.name!r} has a ']' without a matching '['.") if not self.valid_line(line): return @@ -6068,7 +6079,7 @@ def format_docstring_parameters(params: list[Parameter]) -> str: """Create substitution text for {parameters}""" return "".join(p.render_docstring() + "\n" for p in params if p.docstring) - def format_docstring(self) -> str: + def format_docstring(self, lineno: int) -> str: assert self.function is not None f = self.function # For the following special cases, it does not make sense to render a docstring. @@ -6090,9 +6101,10 @@ def format_docstring(self) -> str: lines = f.docstring.split('\n') if len(lines) >= 2: if lines[1]: - fail(f"Docstring for {f.full_name!r} does not have a summary line!\n" - "Every non-blank function docstring must start with " - "a single line summary followed by an empty line.") + self.fail(f"Docstring for {f.full_name!r} does not have a summary line!\n" + "Every non-blank function docstring must start with " + "a single line summary followed by an empty line.", + line_number=lineno) elif len(lines) == 1: # the docstring is only one line right now--the summary line. # add an empty line after the summary line so we have space @@ -6101,7 +6113,8 @@ def format_docstring(self) -> str: parameters_marker_count = len(f.docstring.split('{parameters}')) - 1 if parameters_marker_count > 1: - fail('You may not specify {parameters} more than once in a docstring!') + self.fail('You may not specify {parameters} more than once in a docstring!', + line_number=lineno) # insert signature at front and params after the summary line if not parameters_marker_count: @@ -6136,15 +6149,17 @@ def check_remaining_star(self, lineno: int | None = None) -> None: return break - fail(f"Function {self.function.name!r} specifies {symbol!r} " - f"without following parameters.", line_number=lineno) + self.fail(f"Function {self.function.name!r} specifies {symbol!r} " + f"without following parameters.", + line_number=lineno) def check_previous_star(self, lineno: int | None = None) -> None: assert isinstance(self.function, Function) for p in self.function.parameters.values(): if p.kind == inspect.Parameter.VAR_POSITIONAL: - fail(f"Function {self.function.name!r} uses '*' more than once.") + self.fail(f"Function {self.function.name!r} uses '*' more than once.", + line_number=lineno) def do_post_block_processing_cleanup(self, lineno: int) -> None: @@ -6155,7 +6170,7 @@ def do_post_block_processing_cleanup(self, lineno: int) -> None: return self.check_remaining_star(lineno) - self.function.docstring = self.format_docstring() + self.function.docstring = self.format_docstring(lineno)