Download line_explainer.py from ashritaa-chinnari123/code_explainer_bug_checker_NLP_project: direct link, hf CLI and curl.
- Browser
- Download file 11.3 kB
-
https://huggingface.co/spaces/ashritaa-chinnari123/code_explainer_bug_checker_NLP_project/resolve/main/line_explainer.py
- Command line
-
hf download hf://spaces/ashritaa-chinnari123/code_explainer_bug_checker_NLP_project/line_explainer.py
-
curl -L -o line_explainer.py https://huggingface.co/spaces/ashritaa-chinnari123/code_explainer_bug_checker_NLP_project/resolve/main/line_explainer.py
11.3 kB
| """ | |
| Line-by-line code explainer using Python's `ast` module — v2. | |
| Improvement over v1: instead of just echoing back the code's own syntax | |
| (e.g. "If n < 2, then: Returns False" — which is really just the code | |
| with English scaffolding), this version translates expressions into | |
| actual natural language (e.g. "Checks whether n is less than 2 — if so, | |
| returns False, meaning n is not prime."). | |
| Still fully deterministic (based on the real parsed structure, not a | |
| guess), but reads like an explanation a person would give, not a | |
| transliteration of the syntax. | |
| """ | |
| import ast | |
| # --------------------------------------------------------------------------- | |
| # Expression -> natural English translation | |
| # --------------------------------------------------------------------------- | |
| _CMP_WORDS = { | |
| ast.Lt: "is less than", | |
| ast.LtE: "is less than or equal to", | |
| ast.Gt: "is greater than", | |
| ast.GtE: "is greater than or equal to", | |
| ast.Eq: "is equal to", | |
| ast.NotEq: "is not equal to", | |
| ast.In: "is in", | |
| ast.NotIn: "is not in", | |
| ast.Is: "is", | |
| ast.IsNot: "is not", | |
| } | |
| _BINOP_WORDS = { | |
| ast.Add: "plus", | |
| ast.Sub: "minus", | |
| ast.Mult: "times", | |
| ast.Div: "divided by", | |
| ast.FloorDiv: "divided by (rounded down)", | |
| ast.Mod: "modulo", | |
| ast.Pow: "to the power of", | |
| } | |
| _BOOLOP_WORDS = { | |
| ast.And: "and", | |
| ast.Or: "or", | |
| } | |
| def expr_to_text(node) -> str: | |
| """Recursively turn an AST expression into a natural-English phrase.""" | |
| if node is None: | |
| return "nothing" | |
| if isinstance(node, ast.Constant): | |
| return repr(node.value) | |
| if isinstance(node, ast.Name): | |
| return node.id | |
| if isinstance(node, ast.Attribute): | |
| return f"{expr_to_text(node.value)}.{node.attr}" | |
| if isinstance(node, ast.Subscript): | |
| return f"{expr_to_text(node.value)}[{expr_to_text(node.slice)}]" | |
| if isinstance(node, ast.Compare): | |
| left = expr_to_text(node.left) | |
| parts = [left] | |
| for op, comparator in zip(node.ops, node.comparators): | |
| word = _CMP_WORDS.get(type(op), "compares to") | |
| parts.append(f"{word} {expr_to_text(comparator)}") | |
| return " ".join(parts) | |
| if isinstance(node, ast.BoolOp): | |
| word = _BOOLOP_WORDS.get(type(node.op), "and") | |
| return f" {word} ".join(expr_to_text(v) for v in node.values) | |
| if isinstance(node, ast.UnaryOp): | |
| if isinstance(node.op, ast.Not): | |
| return f"not ({expr_to_text(node.operand)})" | |
| if isinstance(node.op, ast.USub): | |
| return f"negative {expr_to_text(node.operand)}" | |
| return expr_to_text(node.operand) | |
| if isinstance(node, ast.BinOp): | |
| word = _BINOP_WORDS.get(type(node.op), "combined with") | |
| return f"{expr_to_text(node.left)} {word} {expr_to_text(node.right)}" | |
| if isinstance(node, ast.Call): | |
| func_name = expr_to_text(node.func) | |
| args_text = [expr_to_text(a) for a in node.args] | |
| # Friendly phrasing for very common built-ins | |
| if func_name == "range": | |
| if len(args_text) == 1: | |
| return f"every whole number from 0 up to (not including) {args_text[0]}" | |
| elif len(args_text) == 2: | |
| return f"every whole number from {args_text[0]} up to (not including) {args_text[1]}" | |
| elif len(args_text) == 3: | |
| return f"every whole number from {args_text[0]} up to (not including) {args_text[1]}, stepping by {args_text[2]}" | |
| if func_name == "int": | |
| return f"the whole-number part of {args_text[0]}" if args_text else "an integer" | |
| if func_name == "len": | |
| return f"the length of {args_text[0]}" if args_text else "a length" | |
| if func_name == "str": | |
| return f"{args_text[0]} converted to text" if args_text else "a text value" | |
| if func_name in ("sum", "max", "min", "sorted", "reversed", "list", "set"): | |
| verb = { | |
| "sum": "the sum of", | |
| "max": "the largest value in", | |
| "min": "the smallest value in", | |
| "sorted": "a sorted version of", | |
| "reversed": "a reversed version of", | |
| "list": "a list built from", | |
| "set": "a set built from", | |
| }[func_name] | |
| return f"{verb} {args_text[0]}" if args_text else verb | |
| # Generic fallback: "the result of calling foo(a, b)" | |
| joined_args = ", ".join(args_text) | |
| return f"the result of calling {func_name}({joined_args})" | |
| if isinstance(node, ast.List): | |
| return "[" + ", ".join(expr_to_text(e) for e in node.elts) + "]" | |
| if isinstance(node, ast.Tuple): | |
| return "(" + ", ".join(expr_to_text(e) for e in node.elts) + ")" | |
| # Fallback: use ast.unparse if we don't have a specific rule for this node type | |
| try: | |
| return ast.unparse(node) | |
| except Exception: | |
| return "<expression>" | |
| # --------------------------------------------------------------------------- | |
| # Statement -> natural English translation | |
| # --------------------------------------------------------------------------- | |
| def _describe(node, indent=0, class_name=None): | |
| prefix = " " * indent | |
| lines = [] | |
| if isinstance(node, ast.FunctionDef): | |
| args = [a.arg for a in node.args.args] | |
| is_method = bool(args) and args[0] in ("self", "cls") | |
| display_args = args[1:] if is_method else args | |
| arg_text = " and ".join(display_args) if display_args else "no additional input" | |
| if node.name == "__init__": | |
| owner = class_name or "this" | |
| lines.append(f"{prefix}Line {node.lineno}: This is the constructor — it runs automatically whenever a new `{owner}` object is created, setting up {arg_text}.") | |
| elif is_method: | |
| lines.append(f"{prefix}Line {node.lineno}: Defines a method called `{node.name}` (called on instances of this class), which takes {arg_text} as input.") | |
| else: | |
| lines.append(f"{prefix}Line {node.lineno}: Defines a function called `{node.name}`, which takes {arg_text} as input.") | |
| for stmt in node.body: | |
| lines.extend(_describe(stmt, indent + 1, class_name=class_name)) | |
| elif isinstance(node, ast.ClassDef): | |
| bases = [expr_to_text(b) for b in node.bases] | |
| if bases: | |
| lines.append(f"{prefix}Line {node.lineno}: Defines a class called `{node.name}`, which inherits from {', '.join(bases)}.") | |
| else: | |
| lines.append(f"{prefix}Line {node.lineno}: Defines a class called `{node.name}` — a blueprint for creating objects that bundle related data and behavior together.") | |
| for stmt in node.body: | |
| lines.extend(_describe(stmt, indent + 1, class_name=node.name)) | |
| elif isinstance(node, ast.If): | |
| cond = expr_to_text(node.test) | |
| lines.append(f"{prefix}Line {node.lineno}: Checks whether {cond}. If true:") | |
| for stmt in node.body: | |
| lines.extend(_describe(stmt, indent + 1, class_name=class_name)) | |
| if node.orelse: | |
| lines.append(f"{prefix}Otherwise (if that condition is false):") | |
| for stmt in node.orelse: | |
| lines.extend(_describe(stmt, indent + 1, class_name=class_name)) | |
| elif isinstance(node, ast.For): | |
| target = expr_to_text(node.target) | |
| iterable = expr_to_text(node.iter) | |
| lines.append(f"{prefix}Line {node.lineno}: Repeats the following, setting {target} to {iterable}, one at a time:") | |
| for stmt in node.body: | |
| lines.extend(_describe(stmt, indent + 1, class_name=class_name)) | |
| elif isinstance(node, ast.While): | |
| cond = expr_to_text(node.test) | |
| lines.append(f"{prefix}Line {node.lineno}: Keeps repeating the following as long as {cond}:") | |
| for stmt in node.body: | |
| lines.extend(_describe(stmt, indent + 1, class_name=class_name)) | |
| elif isinstance(node, ast.Return): | |
| val = expr_to_text(node.value) if node.value is not None else "nothing" | |
| lines.append(f"{prefix}Line {node.lineno}: Stops the function here and gives back {val}.") | |
| elif isinstance(node, ast.Assign): | |
| targets = " and ".join(expr_to_text(t) for t in node.targets) | |
| val = expr_to_text(node.value) | |
| lines.append(f"{prefix}Line {node.lineno}: Stores {val} in {targets}.") | |
| elif isinstance(node, ast.AugAssign): | |
| target = expr_to_text(node.target) | |
| val = expr_to_text(node.value) | |
| op_word = _BINOP_WORDS.get(type(node.op), "combined with") | |
| lines.append(f"{prefix}Line {node.lineno}: Updates {target} by taking its current value {op_word} {val}.") | |
| elif isinstance(node, ast.Expr): | |
| if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str): | |
| doc_text = node.value.value.strip().splitlines()[0] # first line, in case it's multi-line | |
| lines.append(f'{prefix}Line {node.lineno}: Documentation string: "{doc_text}"') | |
| else: | |
| lines.append(f"{prefix}Line {node.lineno}: Runs {expr_to_text(node.value)}.") | |
| elif isinstance(node, ast.Raise): | |
| if node.exc is not None: | |
| if isinstance(node.exc, ast.Call): | |
| exc_name = expr_to_text(node.exc.func) | |
| exc_args = [expr_to_text(a) for a in node.exc.args] | |
| if exc_args: | |
| lines.append(f"{prefix}Line {node.lineno}: Raises a {exc_name} error with the message {exc_args[0]}.") | |
| else: | |
| lines.append(f"{prefix}Line {node.lineno}: Raises a {exc_name} error.") | |
| else: | |
| lines.append(f"{prefix}Line {node.lineno}: Raises an error — {expr_to_text(node.exc)}.") | |
| else: | |
| lines.append(f"{prefix}Line {node.lineno}: Re-raises the current error.") | |
| elif isinstance(node, ast.Import): | |
| names = ", ".join(a.name for a in node.names) | |
| lines.append(f"{prefix}Line {node.lineno}: Brings in the {names} module so its functions can be used.") | |
| elif isinstance(node, ast.ImportFrom): | |
| names = ", ".join(a.name for a in node.names) | |
| lines.append(f"{prefix}Line {node.lineno}: Brings in {names} from the {node.module} module.") | |
| else: | |
| try: | |
| lines.append(f"{prefix}Line {node.lineno}: {ast.unparse(node)}") | |
| except Exception: | |
| pass | |
| return lines | |
| def explain_line_by_line(code: str): | |
| """ | |
| Returns (success: bool, result) | |
| - If code parses: (True, list_of_explanation_strings) | |
| - If code has a syntax error: (False, error_message_string) | |
| """ | |
| try: | |
| tree = ast.parse(code) | |
| except SyntaxError as e: | |
| return False, f"Can't generate a line-by-line breakdown: the code has a syntax error ({e.msg} at line {e.lineno})." | |
| explanation_lines = [] | |
| for stmt in tree.body: | |
| explanation_lines.extend(_describe(stmt)) | |
| if not explanation_lines: | |
| return True, ["(No statements found to explain.)"] | |
| return True, explanation_lines | |
| if __name__ == "__main__": | |
| sample = """ | |
| def is_prime(n): | |
| if n < 2: | |
| return False | |
| for i in range(2, int(n ** 0.5) + 1): | |
| if n % i == 0: | |
| return False | |
| return True | |
| """ | |
| ok, result = explain_line_by_line(sample) | |
| print("\n".join(result) if ok else result) | |