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) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy