hack-house/.venv/lib/python3.12/site-packages/tracerite/trace_cpy.py
leetcrypt bb1d662ee1 chore: rename project coven → hack-house ⛧
Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"),
README, CLI strings, and branch (coven → hack-house). Gitea repo renamed
cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest +
golden-vector test still green, binary is now `hack-house`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 13:29:14 -07:00

409 lines
14 KiB
Python

# Copied from https://github.com/python/cpython/blob/main/Lib/traceback.py
# We need to use internal functions that are not part of the public API,
# and that are not available in earlier Python versions.
# Unused functionality is removed and we run a formatter.
# One modification is made for Python 3.9 and 3.10 compatibility (see comment).
# ruff: noqa
"""Extract, format and print information about Python stack traces."""
import collections.abc
import itertools
import sys
class _Sentinel:
def __repr__(self):
return "<implicit>"
_sentinel = _Sentinel()
def _parse_value_tb(exc, value, tb):
if (value is _sentinel) != (tb is _sentinel):
raise ValueError("Both or neither of value and tb must be given")
if value is tb is _sentinel:
if exc is not None:
if isinstance(exc, BaseException):
return exc, exc.__traceback__
raise TypeError(f"Exception expected for value, {type(exc).__name__} found")
else:
return None, None
return value, tb
BUILTIN_EXCEPTION_LIMIT = object()
def _safe_string(value, what, func=str):
try:
return func(value)
except:
return f"<{what} {func.__name__}() failed>"
def _walk_tb_with_full_positions(tb):
# Internal version of walk_tb that yields full code positions including
# end line and column information.
while tb is not None:
positions = _get_code_position(tb.tb_frame.f_code, tb.tb_lasti)
# Yield tb_lineno when co_positions does not have a line number to
# maintain behavior with walk_tb.
if positions[0] is None:
yield tb.tb_frame, (tb.tb_lineno,) + positions[1:]
else:
yield tb.tb_frame, positions
tb = tb.tb_next
def _get_code_position(code, instruction_index):
if instruction_index < 0:
return (None, None, None, None)
# TRACERITE MODIFICATION: co_positions() was added in Python 3.11
# Fallback for Python 3.9 and 3.10 compatibility
if not hasattr(code, "co_positions"):
return (None, None, None, None)
positions_gen = code.co_positions()
return next(itertools.islice(positions_gen, instruction_index // 2, None))
def _byte_offset_to_character_offset(str, offset):
as_utf8 = str.encode("utf-8")
return len(as_utf8[:offset].decode("utf-8", errors="replace"))
_Anchors = collections.namedtuple(
"_Anchors",
[
"left_end_lineno",
"left_end_offset",
"right_start_lineno",
"right_start_offset",
"primary_char",
"secondary_char",
],
defaults=["~", "^"],
)
def _extract_caret_anchors_from_line_segment(segment):
"""
Given source code `segment` corresponding to a FrameSummary, determine:
- for binary ops, the location of the binary op
- for indexing and function calls, the location of the brackets.
`segment` is expected to be a valid Python expression.
"""
import ast
try:
# Without parentheses, `segment` is parsed as a statement.
# Binary ops, subscripts, and calls are expressions, so
# we can wrap them with parentheses to parse them as
# (possibly multi-line) expressions.
# e.g. if we try to highlight the addition in
# x = (
# a +
# b
# )
# then we would ast.parse
# a +
# b
# which is not a valid statement because of the newline.
# Adding brackets makes it a valid expression.
# (
# a +
# b
# )
# Line locations will be different than the original,
# which is taken into account later on.
tree = ast.parse(f"(\n{segment}\n)")
except SyntaxError:
return None
if len(tree.body) != 1:
return None
lines = segment.splitlines()
def normalize(lineno, offset):
"""Get character index given byte offset"""
return _byte_offset_to_character_offset(lines[lineno], offset)
def next_valid_char(lineno, col):
"""Gets the next valid character index in `lines`, if
the current location is not valid. Handles empty lines.
"""
while lineno < len(lines) and col >= len(lines[lineno]):
col = 0
lineno += 1
assert lineno < len(lines) and col < len(lines[lineno])
return lineno, col
def increment(lineno, col):
"""Get the next valid character index in `lines`."""
col += 1
lineno, col = next_valid_char(lineno, col)
return lineno, col
def nextline(lineno, col):
"""Get the next valid character at least on the next line"""
col = 0
lineno += 1
lineno, col = next_valid_char(lineno, col)
return lineno, col
def increment_until(lineno, col, stop):
"""Get the next valid non-"\\#" character that satisfies the `stop` predicate"""
while True:
ch = lines[lineno][col]
if ch in "\\#":
lineno, col = nextline(lineno, col)
elif not stop(ch):
lineno, col = increment(lineno, col)
else:
break
return lineno, col
def setup_positions(expr, force_valid=True):
"""Get the lineno/col position of the end of `expr`. If `force_valid` is True,
forces the position to be a valid character (e.g. if the position is beyond the
end of the line, move to the next line)
"""
# -2 since end_lineno is 1-indexed and because we added an extra
# bracket + newline to `segment` when calling ast.parse
lineno = expr.end_lineno - 2
col = normalize(lineno, expr.end_col_offset)
return next_valid_char(lineno, col) if force_valid else (lineno, col)
statement = tree.body[0]
if isinstance(statement, ast.Expr):
expr = statement.value
if isinstance(expr, ast.BinOp):
# ast gives these locations for BinOp subexpressions
# ( left_expr ) + ( right_expr )
# left^^^^^ right^^^^^
lineno, col = setup_positions(expr.left)
# First operator character is the first non-space/')' character
lineno, col = increment_until(
lineno, col, lambda x: not x.isspace() and x != ")"
)
# binary op is 1 or 2 characters long, on the same line,
# before the right subexpression
right_col = col + 1
if (
right_col < len(lines[lineno])
and (
# operator char should not be in the right subexpression
expr.right.lineno - 2 > lineno
or right_col
< normalize(expr.right.lineno - 2, expr.right.col_offset)
)
and not (ch := lines[lineno][right_col]).isspace()
and ch not in "\\#"
):
right_col += 1
# right_col can be invalid since it is exclusive
return _Anchors(lineno, col, lineno, right_col)
if isinstance(expr, ast.Subscript):
# ast gives these locations for value and slice subexpressions
# ( value_expr ) [ slice_expr ]
# value^^^^^ slice^^^^^
# subscript^^^^^^^^^^^^^^^^^^^^
# find left bracket
left_lineno, left_col = setup_positions(expr.value)
left_lineno, left_col = increment_until(
left_lineno, left_col, lambda x: x == "["
)
# find right bracket (final character of expression)
right_lineno, right_col = setup_positions(expr, force_valid=False)
return _Anchors(left_lineno, left_col, right_lineno, right_col)
if isinstance(expr, ast.Call):
# ast gives these locations for function call expressions
# ( func_expr ) (args, kwargs)
# func^^^^^
# call^^^^^^^^^^^^^^^^^^^^^^^^
# find left bracket
left_lineno, left_col = setup_positions(expr.func)
left_lineno, left_col = increment_until(
left_lineno, left_col, lambda x: x == "("
)
# find right bracket (final character of expression)
right_lineno, right_col = setup_positions(expr, force_valid=False)
return _Anchors(left_lineno, left_col, right_lineno, right_col)
return None
_MAX_CANDIDATE_ITEMS = 750
_MAX_STRING_SIZE = 40
_MOVE_COST = 2
_CASE_COST = 1
def _substitution_cost(ch_a, ch_b):
if ch_a == ch_b:
return 0
if ch_a.lower() == ch_b.lower():
return _CASE_COST
return _MOVE_COST
def _compute_suggestion_error(exc_value, tb, wrong_name):
if wrong_name is None or not isinstance(wrong_name, str):
return None
if isinstance(exc_value, AttributeError):
obj = exc_value.obj
try:
try:
d = dir(obj)
except TypeError: # Attributes are unsortable, e.g. int and str
d = list(obj.__class__.__dict__.keys()) + list(obj.__dict__.keys())
d = sorted([x for x in d if isinstance(x, str)])
hide_underscored = wrong_name[:1] != "_"
if hide_underscored and tb is not None:
while tb.tb_next is not None:
tb = tb.tb_next
frame = tb.tb_frame
if "self" in frame.f_locals and frame.f_locals["self"] is obj:
hide_underscored = False
if hide_underscored:
d = [x for x in d if x[:1] != "_"]
except Exception:
return None
elif isinstance(exc_value, ImportError):
try:
mod = __import__(exc_value.name)
try:
d = dir(mod)
except TypeError: # Attributes are unsortable, e.g. int and str
d = list(mod.__dict__.keys())
d = sorted([x for x in d if isinstance(x, str)])
if wrong_name[:1] != "_":
d = [x for x in d if x[:1] != "_"]
except Exception:
return None
else:
assert isinstance(exc_value, NameError)
# find most recent frame
if tb is None:
return None
while tb.tb_next is not None:
tb = tb.tb_next
frame = tb.tb_frame
d = list(frame.f_locals) + list(frame.f_globals) + list(frame.f_builtins)
d = [x for x in d if isinstance(x, str)]
# Check first if we are in a method and the instance
# has the wrong name as attribute
if "self" in frame.f_locals:
self = frame.f_locals["self"]
try:
has_wrong_name = hasattr(self, wrong_name)
except Exception:
has_wrong_name = False
if has_wrong_name:
return f"self.{wrong_name}"
try:
import _suggestions # type: ignore[import]
except ImportError:
pass
else:
return _suggestions._generate_suggestions(d, wrong_name)
# Compute closest match
if len(d) > _MAX_CANDIDATE_ITEMS:
return None
wrong_name_len = len(wrong_name)
if wrong_name_len > _MAX_STRING_SIZE:
return None
best_distance = wrong_name_len
suggestion = None
for possible_name in d:
if possible_name == wrong_name:
# A missing attribute is "found". Don't suggest it (see GH-88821).
continue
# No more than 1/3 of the involved characters should need changed.
max_distance = (len(possible_name) + wrong_name_len + 3) * _MOVE_COST // 6
# Don't take matches we've already beaten.
max_distance = min(max_distance, best_distance - 1)
current_distance = _levenshtein_distance(
wrong_name, possible_name, max_distance
)
if current_distance > max_distance:
continue
if not suggestion or current_distance < best_distance:
suggestion = possible_name
best_distance = current_distance
return suggestion
def _levenshtein_distance(a, b, max_cost):
# A Python implementation of Python/suggestions.c:levenshtein_distance.
# Both strings are the same
if a == b:
return 0
# Trim away common affixes
pre = 0
while a[pre:] and b[pre:] and a[pre] == b[pre]:
pre += 1
a = a[pre:]
b = b[pre:]
post = 0
while a[: post or None] and b[: post or None] and a[post - 1] == b[post - 1]:
post -= 1
a = a[: post or None]
b = b[: post or None]
if not a or not b:
return _MOVE_COST * (len(a) + len(b))
if len(a) > _MAX_STRING_SIZE or len(b) > _MAX_STRING_SIZE:
return max_cost + 1
# Prefer shorter buffer
if len(b) < len(a):
a, b = b, a
# Quick fail when a match is impossible
if (len(b) - len(a)) * _MOVE_COST > max_cost:
return max_cost + 1
# Instead of producing the whole traditional len(a)-by-len(b)
# matrix, we can update just one row in place.
# Initialize the buffer row
row = list(range(_MOVE_COST, _MOVE_COST * (len(a) + 1), _MOVE_COST))
result = 0
for bindex in range(len(b)):
bchar = b[bindex]
distance = result = bindex * _MOVE_COST
minimum = sys.maxsize
for index in range(len(a)):
# 1) Previous distance in this row is cost(b[:b_index], a[:index])
substitute = distance + _substitution_cost(bchar, a[index])
# 2) cost(b[:b_index], a[:index+1]) from previous row
distance = row[index]
# 3) existing result is cost(b[:b_index+1], a[index])
insert_delete = min(result, distance) + _MOVE_COST
result = min(insert_delete, substitute)
# cost(b[:b_index+1], a[:index+1])
row[index] = result
if result < minimum:
minimum = result
if minimum > max_cost:
# Everything in this row is too big, so bail early.
return max_cost + 1
return result