feat(assessment): auto comprehensive assessment - rules engine from college scoring standard, per-semester stats, manual overrides, word export
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
"""Pretty printing for one-shot sampling stack dumps."""
|
||||
|
||||
import contextlib
|
||||
import linecache
|
||||
import os
|
||||
import sys
|
||||
from traceback import _byte_offset_to_character_offset
|
||||
|
||||
import _colorize
|
||||
|
||||
from .collector import extract_lineno, filter_internal_frames, iter_async_frames
|
||||
from .constants import (
|
||||
THREAD_STATUS_GIL_REQUESTED,
|
||||
THREAD_STATUS_HAS_EXCEPTION,
|
||||
THREAD_STATUS_HAS_GIL,
|
||||
THREAD_STATUS_MAIN_THREAD,
|
||||
THREAD_STATUS_ON_CPU,
|
||||
THREAD_STATUS_UNKNOWN,
|
||||
)
|
||||
from .opcode_utils import format_opcode
|
||||
|
||||
|
||||
_STATUS_LABELS = (
|
||||
(THREAD_STATUS_MAIN_THREAD, "main thread"),
|
||||
(THREAD_STATUS_HAS_GIL, "has GIL"),
|
||||
(THREAD_STATUS_ON_CPU, "on CPU"),
|
||||
(THREAD_STATUS_GIL_REQUESTED, "waiting for GIL"),
|
||||
(THREAD_STATUS_HAS_EXCEPTION, "has exception"),
|
||||
)
|
||||
|
||||
|
||||
def _theme_for(file, colorize):
|
||||
if colorize is True:
|
||||
return _colorize.get_theme(force_color=True).profiler_dump
|
||||
if colorize is False:
|
||||
return _colorize.get_theme(force_no_color=True).profiler_dump
|
||||
return _colorize.get_theme(tty_file=file).profiler_dump
|
||||
|
||||
|
||||
def _color(text, color, theme):
|
||||
if not color:
|
||||
return text
|
||||
return f"{color}{text}{theme.reset}"
|
||||
|
||||
|
||||
def _frame_fields(frame):
|
||||
if isinstance(frame, tuple):
|
||||
filename = frame[0] if len(frame) > 0 else ""
|
||||
location = frame[1] if len(frame) > 1 else None
|
||||
qualname = frame[2] if len(frame) > 2 else "<unknown>"
|
||||
opcode = frame[3] if len(frame) > 3 else None
|
||||
else:
|
||||
filename = getattr(frame, "filename", "")
|
||||
location = getattr(frame, "location", None)
|
||||
qualname = getattr(frame, "qualname", None)
|
||||
if qualname is None:
|
||||
qualname = getattr(frame, "funcname", "<unknown>")
|
||||
opcode = getattr(frame, "opcode", None)
|
||||
return filename, location, qualname, opcode
|
||||
|
||||
|
||||
def _location_field(location, index, default=None):
|
||||
if location is None:
|
||||
return default
|
||||
try:
|
||||
value = location[index]
|
||||
except (IndexError, TypeError):
|
||||
return default
|
||||
return default if value is None else value
|
||||
|
||||
|
||||
def _status_text(status):
|
||||
labels = [label for flag, label in _STATUS_LABELS if status & flag]
|
||||
has_state = status & (
|
||||
THREAD_STATUS_HAS_GIL
|
||||
| THREAD_STATUS_GIL_REQUESTED
|
||||
| THREAD_STATUS_HAS_EXCEPTION
|
||||
)
|
||||
if not has_state and not status & (THREAD_STATUS_UNKNOWN | THREAD_STATUS_ON_CPU):
|
||||
labels.append("idle")
|
||||
return ", ".join(labels) if labels else None
|
||||
|
||||
|
||||
def _is_async_dump(stack_frames):
|
||||
return bool(stack_frames) and hasattr(stack_frames[0], "awaited_by")
|
||||
|
||||
|
||||
def _iter_dump_sections(stack_frames):
|
||||
if not stack_frames:
|
||||
return
|
||||
|
||||
if _is_async_dump(stack_frames):
|
||||
for frames, thread_id, leaf_id in iter_async_frames(stack_frames):
|
||||
frames = filter_internal_frames(frames)
|
||||
if frames:
|
||||
yield None, thread_id, None, frames, f"task {leaf_id}"
|
||||
return
|
||||
|
||||
for interpreter_info in stack_frames:
|
||||
interpreter_id = getattr(interpreter_info, "interpreter_id", None)
|
||||
for thread_info in getattr(interpreter_info, "threads", ()):
|
||||
frames = getattr(thread_info, "frame_info", None) or []
|
||||
frames = filter_internal_frames(frames)
|
||||
yield (
|
||||
interpreter_id,
|
||||
getattr(thread_info, "thread_id", None),
|
||||
getattr(thread_info, "status", None),
|
||||
frames,
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def _display_filename(filename):
|
||||
if not filename or filename == "~":
|
||||
return filename
|
||||
with contextlib.suppress(ValueError):
|
||||
relpath = os.path.relpath(filename)
|
||||
if not relpath.startswith(".." + os.sep) and relpath != "..":
|
||||
return relpath
|
||||
return filename
|
||||
|
||||
|
||||
def _format_frame(frame, theme):
|
||||
filename, location, qualname, opcode = _frame_fields(frame)
|
||||
source_filename = filename
|
||||
lineno = extract_lineno(location)
|
||||
qualname_part = _color(qualname, theme.frame, theme)
|
||||
|
||||
if filename == "~" and lineno == 0:
|
||||
line = f" {qualname_part}"
|
||||
else:
|
||||
filename = _display_filename(filename)
|
||||
if filename:
|
||||
file_part = _color(f'"{filename}"', theme.filename, theme)
|
||||
if lineno > 0:
|
||||
line_part = _color(str(lineno), theme.line_no, theme)
|
||||
line = f" File {file_part}, line {line_part}, in {qualname_part}"
|
||||
else:
|
||||
line = f" File {file_part}, in {qualname_part}"
|
||||
else:
|
||||
line = f" {qualname_part}"
|
||||
|
||||
if opcode is not None:
|
||||
line = f"{line} {_color(f'opcode={format_opcode(opcode)}', theme.opcode, theme)}"
|
||||
|
||||
lines = [line]
|
||||
source = _source_line(source_filename, location, lineno, theme)
|
||||
if source:
|
||||
lines.append(f" {source}")
|
||||
return lines
|
||||
|
||||
|
||||
def _source_offsets(line, location, lineno):
|
||||
end_lineno = _location_field(location, 1, lineno)
|
||||
col_offset = _location_field(location, 2, -1)
|
||||
end_col_offset = _location_field(location, 3, -1)
|
||||
|
||||
if col_offset < 0 or end_col_offset < 0 or end_lineno < lineno:
|
||||
return None
|
||||
|
||||
start = _byte_offset_to_character_offset(line, col_offset)
|
||||
if end_lineno == lineno:
|
||||
end = _byte_offset_to_character_offset(line, end_col_offset)
|
||||
else:
|
||||
end = len(line)
|
||||
if start < 0 or end <= start:
|
||||
return None
|
||||
return start, end
|
||||
|
||||
|
||||
def _trim_source_line(line, offsets):
|
||||
stripped = line.lstrip()
|
||||
leading = len(line) - len(stripped)
|
||||
if offsets is None:
|
||||
return stripped, None
|
||||
|
||||
start, end = offsets
|
||||
start = max(start - leading, 0)
|
||||
end = max(end - leading, start + 1)
|
||||
end = min(end, len(stripped))
|
||||
return stripped, (start, end)
|
||||
|
||||
|
||||
def _highlight_source_line(line, offsets, theme):
|
||||
if offsets is None or offsets[1] <= offsets[0]:
|
||||
return _color(line, theme.source, theme)
|
||||
|
||||
start, end = offsets
|
||||
parts = []
|
||||
if line[:start]:
|
||||
parts.append(_color(line[:start], theme.source, theme))
|
||||
parts.append(_color(line[start:end], theme.source_highlight, theme))
|
||||
if line[end:]:
|
||||
parts.append(_color(line[end:], theme.source, theme))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def _source_line(filename, location, lineno, theme):
|
||||
if not filename or filename == "~" or lineno <= 0:
|
||||
return None
|
||||
line = linecache.getline(filename, lineno).removesuffix("\n")
|
||||
if not line:
|
||||
return None
|
||||
|
||||
offsets = _source_offsets(line, location, lineno)
|
||||
line, offsets = _trim_source_line(line, offsets)
|
||||
if not line:
|
||||
return None
|
||||
return _highlight_source_line(line, offsets, theme)
|
||||
|
||||
|
||||
def _section_header(
|
||||
*,
|
||||
pid,
|
||||
interpreter_id,
|
||||
thread_id,
|
||||
status,
|
||||
label,
|
||||
show_pid,
|
||||
show_interpreter,
|
||||
theme,
|
||||
):
|
||||
subject = "Stack dump"
|
||||
if show_pid and pid is not None:
|
||||
subject = f"{subject} for PID {pid}"
|
||||
if thread_id is not None:
|
||||
subject = f"{subject}, thread {thread_id}"
|
||||
|
||||
details = []
|
||||
if show_interpreter and interpreter_id is not None:
|
||||
details.append(f"interpreter {interpreter_id}")
|
||||
if label:
|
||||
details.append(label)
|
||||
if status is not None:
|
||||
status_text = _status_text(status)
|
||||
if status_text:
|
||||
details.append(status_text)
|
||||
|
||||
suffix = "most recent call last"
|
||||
if details:
|
||||
suffix = f"{'; '.join(details)}; {suffix}"
|
||||
return _color(f"{subject} ({suffix}):", theme.header, theme)
|
||||
|
||||
|
||||
def format_stack_dump(stack_frames, *, pid=None, file=None, colorize=None):
|
||||
"""Return a formatted one-shot stack dump."""
|
||||
if file is None:
|
||||
file = sys.stdout
|
||||
|
||||
theme = _theme_for(file, colorize)
|
||||
lines = []
|
||||
sections = list(_iter_dump_sections(stack_frames))
|
||||
if not sections:
|
||||
if pid is None:
|
||||
return f"{_color('No Python stacks found', theme.warning, theme)}\n"
|
||||
return f"{_color(f'No Python stacks found for PID {pid}', theme.warning, theme)}\n"
|
||||
|
||||
interpreter_ids = {
|
||||
interpreter_id
|
||||
for interpreter_id, _thread_id, _status, _frames, _label in sections
|
||||
if interpreter_id is not None
|
||||
}
|
||||
show_interpreter = len(interpreter_ids) > 1
|
||||
|
||||
for section_index, (interpreter_id, thread_id, status, frames, label) in enumerate(sections):
|
||||
if section_index:
|
||||
lines.append("")
|
||||
lines.append(
|
||||
_section_header(
|
||||
pid=pid,
|
||||
interpreter_id=interpreter_id,
|
||||
thread_id=thread_id,
|
||||
status=status,
|
||||
label=label,
|
||||
show_pid=section_index == 0,
|
||||
show_interpreter=show_interpreter,
|
||||
theme=theme,
|
||||
)
|
||||
)
|
||||
|
||||
if not frames:
|
||||
lines.append(_color("No Python frames", theme.warning, theme))
|
||||
continue
|
||||
|
||||
for frame in reversed(frames):
|
||||
lines.extend(_format_frame(frame, theme))
|
||||
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def print_stack_dump(stack_frames, *, pid=None, file=None, colorize=None):
|
||||
"""Pretty-print a one-shot stack dump."""
|
||||
if file is None:
|
||||
file = sys.stdout
|
||||
file.write(format_stack_dump(stack_frames, pid=pid, file=file, colorize=colorize))
|
||||
Reference in New Issue
Block a user