Display trace frames independent of the current issue.

        Parameters (all optional):
            callers: str or list[str]            filter traces by this caller name
            callees: str or list[str]            filter traces by this callee name
            kind: precondition|postcondition    the type of trace frames to show
            limit: int (default: 10)            how many trace frames to display
                                                (specify limit=None for all)

        Sample usage:
            frames callers="module.function", kind=postcondition

        String filters support LIKE wildcards (%, _) from SQL:
            % matches anything (like .* in regex)
            _ matches 1 character (like . in regex)

def frames(
        self,
        *,
        callers: Optional[Union[str, List[str]]] = None,
        callees: Optional[Union[str, List[str]]] = None,
        kind: Optional[TraceKind] = None,
        limit: Optional[int] = 10,
    ):
        """Display trace frames independent of the current issue.

        Parameters (all optional):
            callers: str or list[str]            filter traces by this caller name
            callees: str or list[str]            filter traces by this callee name
            kind: precondition|postcondition    the type of trace frames to show
            limit: int (default: 10)            how many trace frames to display
                                                (specify limit=None for all)

        Sample usage:
            frames callers="module.function", kind=postcondition

        String filters support LIKE wildcards (%, _) from SQL:
            % matches anything (like .* in regex)
            _ matches 1 character (like . in regex)
        """
        with self.db.make_session() as session:
            query = (
                session.query(
                    TraceFrame.id,
                    CallerText.contents.label("caller"),
                    TraceFrame.caller_port,
                    CalleeText.contents.label("callee"),
                    TraceFrame.callee_port,
                )
                .filter(TraceFrame.run_id == self.current_run_id)
                .join(CallerText, CallerText.id == TraceFrame.caller_id)
                .join(CalleeText, CalleeText.id == TraceFrame.callee_id)
            )

            if callers is not None:
                query = self._add_list_or_string_filter_to_query(
                    callers, query, CallerText.contents, "callers"
                )

            if callees is not None:
                query = self._add_list_or_string_filter_to_query(
                    callees, query, CalleeText.contents, "callees"
                )

            if kind is not None:
                if kind not in {TraceKind.PRECONDITION, TraceKind.POSTCONDITION}:
                    raise UserError(
                        "Try 'frames kind=postcondition'"
                        " or 'frames kind=precondition'."
                    )
                query = query.filter(TraceFrame.kind == kind)

            if limit is not None and not isinstance(limit, int):
                raise UserError("'limit' should be an int or None.")

            trace_frames = query.group_by(TraceFrame.id).order_by(
                CallerText.contents, CalleeText.contents
            )

            total_trace_frames = trace_frames.count()
            limit = limit or total_trace_frames

            self._output_trace_frames(
                self._group_trace_frames(trace_frames, limit), limit, total_trace_frames
            )