e0336379f5
CI / lint (pull_request) Successful in 29s
CI / typecheck (pull_request) Successful in 48s
CI / quality (pull_request) Successful in 33s
CI / security (pull_request) Successful in 1m2s
CI / build (pull_request) Successful in 23s
CI / helm (pull_request) Successful in 23s
CI / unit_tests (pull_request) Successful in 6m48s
CI / e2e_tests (pull_request) Successful in 19m4s
CI / integration_tests (pull_request) Successful in 22m6s
CI / coverage (pull_request) Successful in 10m59s
CI / docker (pull_request) Successful in 22s
CI / status-check (pull_request) Successful in 2s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Successful in 57m5s
Rewrites the block_stack management in fix_python_indentation to correctly handle try...except...finally blocks in all configurations: - Single except clause - Multiple except clauses for the same try - try...except...finally - Nested try blocks with outer except - Nested try with inner finally followed by outer except (critical regression) Root cause: The original implementation stored only keyword strings in block_stack (e.g. 'try', 'class', 'other'). Two bugs existed: 1. When 'finally:' was encountered, the code popped the 'try' from the stack. This meant subsequent 'except:' clauses for an outer try could not find their matching try, producing syntactically invalid Python. 2. The dedent calculation for 'except'/'finally' counted all stack entries up to the first 'try', but did not account for nested try blocks where the inner try's except/finally had already been processed. Fix: Replace the flat string stack with a tuple stack of (keyword, base_indent, has_seen_except_finally). The new logic: - 'finally:' always belongs to the innermost try. Pop non-try entries, set indent to that try's base_indent, then pop the try itself (finally closes the block). The outer try remains on the stack. - 'except:' belongs to the innermost try. If the innermost try has already seen an except/finally AND there is an outer try, the inner try is done: pop it and use the outer try instead. Otherwise use the innermost try and mark it as having seen an except/finally clause. Adds a Behave BDD feature (tdd_indentation_library_try_except.feature) with 6 scenarios covering all acceptance criteria from issue #2845. ISSUES CLOSED: #2845
129 lines
5.3 KiB
Python
129 lines
5.3 KiB
Python
"""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)
|