"""Robot library providing helpers for inline Python scripts.""" from __future__ import annotations def fix_python_indentation(script: str) -> str: """Reconstruct indentation lost when Robot strips leading spaces. Args: script: Source code with indentation removed from the start of each line. Returns: Script with indentation restored so Python can execute it. """ lines = script.split("\n") result: list[str] = [] indent_level = 0 # Each entry is a tuple (keyword, base_indent, has_seen_except_finally) where: # keyword — "try", "class", or "other" # base_indent — the indent level at which the block keyword was written # has_seen_except_finally — True once an except/finally clause has been seen # for this try block (used to detect when the inner # try is done and an outer try's except/finally follows) block_stack: list[tuple[str, int, bool]] = [] for line in lines: stripped = line.strip() if not stripped: result.append("") continue is_dedenting = stripped.startswith( ("else:", "elif ", "except:", "except ", "finally:") ) # Check if we should dedent from a class/function body back to module level # This happens when we see import, class, or a top-level statement after a class is_module_level_statement = False # If we're inside a class and see an import, we need to dedent if ( indent_level > 0 and block_stack and ( stripped.startswith(("import ", "from ")) or (stripped.startswith("class ") and block_stack[-1][0] == "class") ) ): is_module_level_statement = True if is_dedenting and block_stack: if stripped.startswith("finally:"): # ``finally`` always belongs to the innermost ``try`` on the stack. # Pop non-try entries above it, set indent to the try's level, # then pop the try itself (``finally`` closes the try block). while block_stack and block_stack[-1][0] != "try": block_stack.pop() if block_stack and block_stack[-1][0] == "try": indent_level = block_stack[-1][1] block_stack.pop() else: indent_level = max(0, indent_level - 1) elif stripped.startswith(("except:", "except ")): # ``except`` belongs to the innermost ``try``. However, if the # innermost ``try`` has already seen an ``except``/``finally`` # clause AND there is an outer ``try`` on the stack, this # ``except`` belongs to the outer ``try`` (the inner try block # is complete). while block_stack and block_stack[-1][0] != "try": block_stack.pop() if block_stack and block_stack[-1][0] == "try": innermost_try = block_stack[-1] has_outer_try = any(b[0] == "try" for b in block_stack[:-1]) if innermost_try[2] and has_outer_try: # Inner try is done — pop it and find the outer try. block_stack.pop() while block_stack and block_stack[-1][0] != "try": block_stack.pop() if block_stack and block_stack[-1][0] == "try": indent_level = block_stack[-1][1] # Mark this try as having seen an except/finally clause. block_stack[-1] = ( block_stack[-1][0], block_stack[-1][1], True, ) else: indent_level = max(0, indent_level - 1) else: indent_level = max(0, indent_level - 1) else: # ``else`` / ``elif`` — dedent one level from the preceding body. indent_level = max(0, indent_level - 1) elif is_module_level_statement: # Dedent back to module level indent_level = 0 block_stack.clear() current_indent = indent_level result.append(" " * current_indent + stripped) # Update the stack and indent level for the *next* line. if stripped.startswith("try:"): block_stack.append(("try", current_indent, False)) indent_level += 1 elif stripped.startswith("finally:"): # The matching ``try`` was already popped during the dedent phase above. indent_level += 1 elif stripped.startswith(("except:", "except ")): # The try remains on the stack (more except/finally may follow). indent_level += 1 elif stripped.endswith(":") and not is_dedenting: if stripped.startswith("class "): block_stack.append(("class", current_indent, False)) else: block_stack.append(("other", current_indent, False)) indent_level += 1 elif is_dedenting and stripped.endswith(":"): indent_level += 1 return "\n".join(result)