#@+leo-ver=5-thin
#@+node:ekr.20170302123956.1: * @file ../doc/leoAttic.txt
# This is Leo's final resting place for dead code.
# New in Leo 6.7.5. The attic will contain only code retired in the present release.

#@@language python
#@@killbeautify
#@+all
#@+node:ekr.20240617085704.1: ** Permanent attic
#@+node:ekr.20230913144248.1: *3* g.SherlockTracer
# I am going to leave this class in the attic indefinitely.
# It might be useful as the base for other classes.
#@+node:ekr.20121128031949.12605: *4* class g.SherlockTracer
class SherlockTracer:
    """
    A stand-alone tracer class with many of Sherlock's features.

    This class should work in any environment containing the re, os and sys modules.

    The arguments in the pattern lists determine which functions get traced
    or which stats get printed. Each pattern starts with "+", "-", "+:" or
    "-:", followed by a regular expression::

    "+x"  Enables tracing (or stats) for all functions/methods whose name
          matches the regular expression x.
    "-x"  Disables tracing for functions/methods.
    "+:x" Enables tracing for all functions in the **file** whose name matches x.
    "-:x" Disables tracing for an entire file.

    Enabling and disabling depends on the order of arguments in the pattern
    list. Consider the arguments for the Rope trace::

    patterns=['+.*','+:.*',
        '-:.*\\lib\\.*','+:.*rope.*','-:.*leoGlobals.py',
        '-:.*worder.py','-:.*prefs.py','-:.*resources.py',])

    This enables tracing for everything, then disables tracing for all
    library modules, except for all rope modules. Finally, it disables the
    tracing for Rope's worder, prefs and resources modules.

    Being able to zero in on the code of interest can be a big help in
    studying other people's code. This is a non-invasive method: no tracing
    code needs to be inserted anywhere.

    Usage:

    g.SherlockTracer(patterns).run()
    """
    @others
#@+node:ekr.20121128031949.12602: *5* sherlock.__init__
def __init__(
    self,
    patterns: list[Any],
    indent: bool = True,
    show_args: bool = True,
    show_return: bool = True,
    verbose: bool = True,
) -> None:
    """SherlockTracer ctor."""
    self.bad_patterns: list[str] = []  # List of bad patterns.
    self.indent = indent  # True: indent calls and returns.
    self.contents_d: dict[str, list] = {}  # Keys are file names, values are file lines.
    self.n = 0  # The frame level on entry to run.
    self.stats: dict[str, dict] = {}  # Keys are full file names, values are dicts.
    self.patterns: list[Any] = None  # A list of regex patterns to match.
    self.pattern_stack: list[str] = []
    self.show_args = show_args  # True: show args for each function call.
    self.show_return = show_return  # True: show returns from each function.
    self.trace_lines = True  # True: trace lines in enabled functions.
    self.verbose = verbose  # True: print filename:func
    self.set_patterns(patterns)
    try:  # Don't assume g.app exists.
        from leo.core.leoQt import QtCore
        if QtCore:
            # pylint: disable=no-member
            QtCore.pyqtRemoveInputHook()
    except Exception:
        pass
#@+node:ekr.20140326100337.16844: *5* sherlock.__call__
def __call__(self, frame: Any, event: Any, arg: Any) -> Any:
    """Exists so that self.dispatch can return self."""
    return self.dispatch(frame, event, arg)
#@+node:ekr.20140326100337.16846: *5* sherlock.bad_pattern
def bad_pattern(self, pattern: Any) -> None:
    """Report a bad Sherlock pattern."""
    if pattern not in self.bad_patterns:
        self.bad_patterns.append(pattern)
        print(f"\nignoring bad pattern: {pattern}\n")
#@+node:ekr.20140326100337.16847: *5* sherlock.check_pattern
def check_pattern(self, pattern: str) -> bool:
    """Give an error and return False for an invalid pattern."""
    try:
        for prefix in ('+:', '-:', '+', '-'):
            if pattern.startswith(prefix):
                re.match(pattern[len(prefix) :], 'xyzzy')
                return True
        self.bad_pattern(pattern)
        return False
    except Exception:
        self.bad_pattern(pattern)
        return False
#@+node:ekr.20121128031949.12609: *5* sherlock.dispatch
def dispatch(self, frame: Any, event: Any, arg: Any) -> Any:
    """The dispatch method."""
    if event == 'call':
        self.do_call(frame, arg)
    elif event == 'return' and self.show_return:
        self.do_return(frame, arg)
    elif event == 'line' and self.trace_lines:
        self.do_line(frame, arg)
    # Queue the SherlockTracer instance again.
    return self
#@+node:ekr.20121128031949.12603: *5* sherlock.do_call & helper
def do_call(self, frame: Any, unused_arg: Any) -> None:
    """Trace through a function call."""
    frame1 = frame
    code = frame.f_code
    file_name = code.co_filename
    locals_ = frame.f_locals
    function_name = code.co_name
    try:
        full_name = self.get_full_name(locals_, function_name)
    except Exception:
        full_name = function_name
    if not self.is_enabled(file_name, full_name, self.patterns):
        # 2020/09/09: Don't touch, for example, __ methods.
        return
    n = 0  # The number of callers of this def.
    while frame:
        frame = frame.f_back
        n += 1
    indent = ' ' * max(0, n - self.n) if self.indent else ''
    path = f"{os.path.basename(file_name):>20}" if self.verbose else ''
    leadin = '+' if self.show_return else ''
    args_list = self.get_args(frame1)
    if self.show_args and args_list:
        args_s = ','.join(args_list)
        args_s2 = f"({args_s})"
        if len(args_s2) > 100:
            print(f"{path}:{indent}{leadin}{full_name}")
            g.printObj(args_list, indent=len(indent) + 22)
        else:
            print(f"{path}:{indent}{leadin}{full_name}{args_s2}")
    else:
        print(f"{path}:{indent}{leadin}{full_name}")
    # Always update stats.
    d = self.stats.get(file_name, {})
    d[full_name] = 1 + d.get(full_name, 0)
    self.stats[file_name] = d
#@+node:ekr.20130111185820.10194: *6* sherlock.get_args
def get_args(self, frame: Any) -> list[str]:
    """Return a list of string "name=val" for each arg in the function call."""
    code = frame.f_code
    locals_ = frame.f_locals
    name = code.co_name
    n = code.co_argcount
    if code.co_flags & 4:
        n = n + 1
    if code.co_flags & 8:
        n = n + 1
    result = []
    for i in range(n):
        name = code.co_varnames[i]
        if name != 'self':
            arg = locals_.get(name, '*undefined*')
            if arg:
                if isinstance(arg, (list, tuple)):
                    val_s = ','.join([self.show(z) for z in arg if self.show(z)])
                    val = f"[{val_s}]"
                elif isinstance(arg, str):
                    val = arg
                else:
                    val = self.show(arg)
                if val:
                    result.append(f"{name}={val}")
    return result
#@+node:ekr.20140402060647.16845: *5* sherlock.do_line (not used)
bad_fns: list[str] = []

def do_line(self, frame: Any, arg: Any) -> None:
    """print each line of enabled functions."""
    if 1:
        return
    code = frame.f_code
    file_name = code.co_filename
    locals_ = frame.f_locals
    name = code.co_name
    full_name = self.get_full_name(locals_, name)
    if not self.is_enabled(file_name, full_name, self.patterns):
        return
    n = frame.f_lineno - 1  # Apparently, the first line is line 1.
    d = self.contents_d
    lines = d.get(file_name)
    if not lines:
        print(file_name)
        try:
            with open(file_name) as f:
                s = f.read()
        except Exception:
            if file_name not in self.bad_fns:
                self.bad_fns.append(file_name)
                print(f"open({file_name}) failed")
            return
        lines = g.splitLines(s)
        d[file_name] = lines
    line = lines[n].rstrip() if n < len(lines) else '<EOF>'
    if 0:
        print(f"{name:3} {line}")
    else:
        print(f"{g.shortFileName(file_name)} {n} {full_name} {line}")
#@+node:ekr.20130109154743.10172: *5* sherlock.do_return & helper
def do_return(self, frame: Any, arg: Any) -> None:  # Arg *is* used below.
    """Trace a return statement."""
    code = frame.f_code
    fn = code.co_filename
    locals_ = frame.f_locals
    name = code.co_name
    self.full_name = self.get_full_name(locals_, name)
    if not self.is_enabled(fn, self.full_name, self.patterns):
        return
    n = 0
    while frame:
        frame = frame.f_back
        n += 1
    path = f"{os.path.basename(fn):>20}" if self.verbose else ''
    if name and name == '__init__':
        try:
            ret1 = locals_ and locals_.get('self', None)
            self.put_ret(ret1, n, path)
        except NameError:
            self.put_ret(f"<{ret1.__class__.__name__}>", n, path)
    else:
        self.put_ret(arg, n, path)
#@+node:ekr.20220605141445.1: *6* sherlock.put_ret
def put_ret(self, arg: Any, n: int, path: str) -> None:
    """Print arg, the value returned by a "return" statement."""
    indent = ' ' * max(0, n - self.n + 1) if self.indent else ''
    try:
        if isinstance(arg, types.GeneratorType):
            ret = '<generator>'
        elif isinstance(arg, (tuple, list)):
            ret_s = ','.join([self.show(z) for z in arg])
            if len(ret_s) > 40:
                g.printObj(arg, indent=len(indent))
                ret = ''
            else:
                ret = f"[{ret_s}]"
        elif arg:
            ret = self.show(arg)
            if len(ret) > 100:
                ret = f"\n    {ret}"
        else:
            ret = '' if arg is None else repr(arg)
        print(f"{path}:{indent}-{self.full_name} -> {ret}")
    except Exception:
        exctype, value = sys.exc_info()[:2]
        try:  # Be extra careful.
            arg_s = f"arg: {arg!r}"
        except Exception:
            arg_s = ''  # arg.__class__.__name__
        print(
            f"{path}:{indent}-{self.full_name} -> "
            f"{exctype.__name__}, {value} {arg_s}"
        )
#@+node:ekr.20121128111829.12185: *5* sherlock.fn_is_enabled
def fn_is_enabled(self, func: Any, patterns: list[str]) -> bool:
    """Return True if tracing for the given function is enabled."""
    if func in self.ignored_functions:
        return False

    def ignore_function() -> None:
        if func not in self.ignored_functions:
            self.ignored_functions.append(func)
            print(f"Ignore function: {func}")
    #
    # New in Leo 6.3. Never trace dangerous functions.
    table = (
        '_deepcopy.*',
        # Unicode primitives.
        'encode\b', 'decode\b',
        # System functions
        '.*__next\b',
        '<frozen>', '<genexpr>', '<listcomp>',
        # '<decorator-gen-.*>',
        'get\b',
        # String primitives.
        'append\b', 'split\b', 'join\b',
        # File primitives...
        'access_check\b', 'expanduser\b', 'exists\b', 'find_spec\b',
        'abspath\b', 'normcase\b', 'normpath\b', 'splitdrive\b',
    )
    g.trace('=====', func)
    for z in table:
        if re.match(z, func):
            ignore_function()
            return False
    #
    # Legacy code.
    try:
        enabled, pattern = False, None
        for pattern in patterns:
            if pattern.startswith('+:'):
                if re.match(pattern[2:], func):
                    enabled = True
            elif pattern.startswith('-:'):
                if re.match(pattern[2:], func):
                    enabled = False
        return enabled
    except Exception:
        self.bad_pattern(pattern)
        return False
#@+node:ekr.20130112093655.10195: *5* sherlock.get_full_name
def get_full_name(self, locals_: Any, name: str) -> str:
    """Return class_name::name if possible."""
    full_name = name
    try:
        user_self = locals_ and locals_.get('self', None)
        if user_self:
            full_name = user_self.__class__.__name__ + '::' + name
    except Exception:
        pass
    return full_name
#@+node:ekr.20121128111829.12183: *5* sherlock.is_enabled
ignored_files: list[str] = []  # List of files.
ignored_functions: list[str] = []  # List of files.

def is_enabled(
    self,
    file_name: str,
    function_name: str,
    patterns: list[str] = None,
) -> bool:
    """Return True if tracing for function_name in the given file is enabled."""
    #
    # New in Leo 6.3. Never trace through some files.
    if not os:
        return False  # Shutting down.
    base_name = os.path.basename(file_name)
    if base_name in self.ignored_files:
        return False

    def ignore_file() -> None:
        if base_name not in self.ignored_files:
            self.ignored_files.append(base_name)

    def ignore_function() -> None:
        if function_name not in self.ignored_functions:
            self.ignored_functions.append(function_name)

    if f"{os.sep}lib{os.sep}" in file_name:
        ignore_file()
        return False
    if base_name.startswith('<') and base_name.endswith('>'):
        ignore_file()
        return False
    #
    # New in Leo 6.3. Never trace dangerous functions.
    table = (
        '_deepcopy.*',
        # Unicode primitives.
        'encode\b', 'decode\b',
        # System functions
        '.*__next\b',
        '<frozen>', '<genexpr>', '<listcomp>',
        # '<decorator-gen-.*>',
        'get\b',
        # String primitives.
        'append\b', 'split\b', 'join\b',
        # File primitives...
        'access_check\b', 'expanduser\b', 'exists\b', 'find_spec\b',
        'abspath\b', 'normcase\b', 'normpath\b', 'splitdrive\b',
    )
    for z in table:
        if re.match(z, function_name):
            ignore_function()
            return False
    #
    # Legacy code.
    enabled = False
    if patterns is None:
        patterns = self.patterns
    for pattern in patterns:
        try:
            if pattern.startswith('+:'):
                if re.match(pattern[2:], file_name):
                    enabled = True
            elif pattern.startswith('-:'):
                if re.match(pattern[2:], file_name):
                    enabled = False
            elif pattern.startswith('+'):
                if re.match(pattern[1:], function_name):
                    enabled = True
            elif pattern.startswith('-'):
                if re.match(pattern[1:], function_name):
                    enabled = False
            else:
                self.bad_pattern(pattern)
        except Exception:
            self.bad_pattern(pattern)
    return enabled
#@+node:ekr.20121128111829.12182: *5* sherlock.print_stats
def print_stats(self, patterns: list[str] = None) -> None:
    """Print all accumulated statisitics."""
    print('\nSherlock statistics...')
    if not patterns:
        patterns = ['+.*', '+:.*',]
    for fn in sorted(self.stats.keys()):
        d = self.stats.get(fn)
        if self.fn_is_enabled(fn, patterns):
            result = sorted(d.keys())  # type:ignore
        else:
            result = [key for key in sorted(d.keys())  # type:ignore
                if self.is_enabled(fn, key, patterns)]
        if result:
            print('')
            fn = fn.replace('\\', '/')
            parts = fn.split('/')
            print('/'.join(parts[-2:]))
            for key in result:
                print(f"{d.get(key):4} {key}")
#@+node:ekr.20121128031949.12614: *5* sherlock.run
# Modified from pdb.Pdb.set_trace.

def run(self, frame: Any = None) -> None:
    """Trace from the given frame or the caller's frame."""
    print("SherlockTracer.run:patterns:\n%s" % '\n'.join(self.patterns))
    if frame is None:
        frame = sys._getframe().f_back
    # Compute self.n, the number of frames to ignore.
    self.n = 0
    while frame:
        frame = frame.f_back
        self.n += 1
    # Pass self to sys.settrace to give easy access to all methods.
    sys.settrace(self)
#@+node:ekr.20140322090829.16834: *5* sherlock.push & pop
def push(self, patterns: list[str]) -> None:
    """Push the old patterns and set the new."""
    self.pattern_stack.append(self.patterns)  # type:ignore
    self.set_patterns(patterns)
    print(f"SherlockTracer.push: {self.patterns}")

def pop(self) -> None:
    """Restore the pushed patterns."""
    if self.pattern_stack:
        self.patterns = self.pattern_stack.pop()  # type:ignore
        print(f"SherlockTracer.pop: {self.patterns}")
    else:
        print('SherlockTracer.pop: pattern stack underflow')
#@+node:ekr.20140326100337.16845: *5* sherlock.set_patterns
def set_patterns(self, patterns: list[str]) -> None:
    """Set the patterns in effect."""
    self.patterns = [z for z in patterns if self.check_pattern(z)]
#@+node:ekr.20140322090829.16831: *5* sherlock.show
def show(self, item: Any) -> str:
    """return the best representation of item."""
    if not item:
        return repr(item)
    if isinstance(item, dict):
        return 'dict'
    if isinstance(item, str):
        s = repr(item)
        if len(s) <= 20:
            return s
        return s[:17] + '...'
    s = repr(item)
    # A Hack for mypy:
    if s.startswith("<object object"):
        s = "_dummy"
    return s
#@+node:ekr.20121128093229.12616: *5* sherlock.stop
def stop(self) -> None:
    """Stop all tracing."""
    sys.settrace(None)
#@+node:ekr.20181202062518.1: *3* leoFastRedraw.py
"""
Gui-independent fast-redraw code.

For an explanation, see this thread:
https://groups.google.com/forum/#!topic/leo-editor/hpHyHU2sWtM
"""
import difflib
import re
import time
from leo.core import leoGlobals as g

class FastRedraw:
    @others

@language python
@tabwidth -4
@pagewidth 70
#@+node:ekr.20181202060924.4: *4* LeoGui.dump_diff_op_codes
def dump_diff_op_codes(self, a, b, op_codes):
    """Dump the opcodes returned by difflib.SequenceMatcher."""

    def summarize(aList):
        pat = re.compile(r'.*:.*:(.*)')
        return ', '.join([pat.match(z).group(1) for z in aList])

    for tag, i1, i2, j1, j2 in op_codes:
        if tag == 'equal':
            print(f"{tag:7} at {i1}:{i2} (both) ==> {summarize(b[j1:j2])!r}")
        elif tag == 'insert':
            print(f"{tag:7} at {i1}:{i2} (b)    ==> {summarize(b[j1:j2])!r}")
        elif tag == 'delete':
            print(f"{tag:7} at {i1}:{i2} (a)    ==> {summarize(a[i1:i2])!r}")
        elif tag == 'replace':
            print(f"{tag:7} at {i1}:{i2} (a)    ==> {summarize(a[i1:i2])!r}")
            print(f"{tag:7} at {i1}:{i2} (b)    ==> {summarize(b[j1:j2])!r}")
        else:
            print('unknown tag')
#@+node:ekr.20181202060924.5: *4* LeoGui.dump_opcodes
def dump_opcodes(self, opcodes):
    """Dump the opcodes returned by app.peep_hole and app.make_redraw_list."""
    for z in opcodes:
        kind = z[0]
        if kind == 'replace':
            kind, i1, gnxs1, gnxs2 = z
            print(kind, i1)
            print("  a: [%s]" % ',\n    '.join(gnxs1))
            print("  b: [%s]" % ',\n    '.join(gnxs2))
        elif kind in ('delete', 'insert'):
            kind, i1, gnxs = z
            print(kind, i1)
            print("  [%s]" % ',\n    '.join(gnxs))
        else:
            print(z)
#@+node:ekr.20181202060924.2: *4* LeoGui.flatten_outline
def flatten_outline(self, c):
    """Return a flat list of strings "level:gnx" for all *visible* positions."""
    trace = False and not g.unitTesting
    t1 = time.process_time()
    aList: list[str] = []
    for p in c.rootPosition().self_and_siblings():
        self.extend_flattened_outline(aList, p)
    if trace:
        t2 = time.process_time()
        print(f"app.flatten_outline: {len(aList)} entries {t2 - t1:6.4f} sec.")
    return aList

def extend_flattened_outline(self, aList, p):
    """Add p and all p's visible descendants to aList."""
    # Padding the fields causes problems later.
    aList.append(f"{p.level()}:{p.gnx}:{p.h}\n")
    if p.isExpanded():
        for child in p.children():
            self.extend_flattened_outline(aList, child)
#@+node:ekr.20181202060924.3: *4* LeoGui.make_redraw_list
def make_redraw_list(self, a, b):
    """
    Diff the a (old) and b (new) outline lists.
    Then optimize the diffs to create a redraw instruction list.
    """
    trace = False and not g.unitTesting
    if a == b:
        return []

    def gnxs(aList):
        """Return the gnx list. Do not try to remove this!"""
        return [z.strip() for z in aList]
    @others # Define local helpers
    d = difflib.SequenceMatcher(None, a, b)
    op_codes = list(d.get_opcodes())
    # dump_diff_op_codes(a, b, op_codes)
    #
    # Generate the instruction list, and verify the result.
    opcodes, result = [], []
    for tag, i1, i2, j1, j2 in op_codes:
        if tag == 'insert':
            opcodes.append(['insert', i1, gnxs(b[j1:j2])])
        elif tag == 'delete':
            opcodes.append(['delete', i1, gnxs(a[i1:i2])])
        elif tag == 'replace':
            opcodes.append(['replace', i1, gnxs(a[i1:i2]), gnxs(b[j1:j2])])
        result.extend(b[j1:j2])
    assert b == result, (a, b)
    #
    # Run the peephole.
    opcodes = self.peep_hole(opcodes)
    if trace:
        print('app.make_redraw_list: opcodes after peephole...')
        self.dump_opcodes(opcodes)
    return opcodes
#@+node:ekr.20181202060924.6: *4* LeoGui.peep_hole
def peep_hole(self, opcodes):
    """Scan the list of opcodes, merging adjacent op-codes."""
    i, result = 0, []
    while i < len(opcodes):
        op0 = opcodes[i]
        if i == len(opcodes) - 1:
            result.append(op0)
            break
        op1 = opcodes[i + 1]
        kind0, kind1 = op0[0], op1[0]
        # Merge adjacent insert/delete opcodes with the same gnx.
        if (
            kind0 == 'insert' and kind1 == 'delete' or
            kind0 == 'delete' and kind1 == 'insert'
        ):
            kind0, index0, gnxs0 = op0
            kind1, index1, gnxs1 = op1
            if gnxs0[0] == gnxs1[0]:
                result.append(['move', index0, index1, gnxs0, gnxs1])
                i += 2  # Don't scan either op again!
                break
        # The default is to retain the opcode.
        result.append(op0)
        i += 1
    return result
#@-all
#@@nosearch
#@-leo
