# -*- coding: utf-8 -*-
"""
pdb++, a drop-in replacement for pdb
====================================

This module extends the stdlib pdb in numerous ways: look at the README for
more details on pdb++ features.
"""

from __future__ import print_function

import sys
import os.path
import inspect
import code
import codecs
import contextlib
import types
import traceback
import subprocess
import threading
import pprint
import re
import signal
from collections import OrderedDict

import fancycompleter
import six
from fancycompleter import Color, Completer, ConfigurableClass

__author__ = 'Antonio Cuni <anto.cuni@gmail.com>'
__url__ = 'http://github.com/antocuni/pdb'
__version__ = fancycompleter.LazyVersion('pdbpp')

try:
    from inspect import signature  # Python >= 3.3
except ImportError:
    try:
        from funcsigs import signature
    except ImportError:
        def signature(obj):
            return ' [pip install funcsigs to show the signature]'

try:
    from functools import lru_cache
except ImportError:
    from functools import wraps

    def lru_cache(maxsize):
        """Simple cache (with no maxsize basically) for py27 compatibility.

        Given that pdb there uses linecache.getline for each line with
        do_list a cache makes a big differene."""

        def dec(fn, *args):
            cache = {}

            @wraps(fn)
            def wrapper(*args):
                key = args
                try:
                    ret = cache[key]
                except KeyError:
                    ret = cache[key] = fn(*args)
                return ret

            return wrapper

        return dec

# If it contains only _, digits, letters, [] or dots, it's probably side
# effects free.
side_effects_free = re.compile(r'^ *[_0-9a-zA-Z\[\].]* *$')

RE_COLOR_ESCAPES = re.compile("(\x1b[^m]+m)+")
RE_REMOVE_FANCYCOMPLETER_ESCAPE_SEQS = re.compile(r"\x1b\[[\d;]+m")

if sys.version_info < (3, ):
    from io import BytesIO as StringIO
else:
    from io import StringIO


local = threading.local()
local.GLOBAL_PDB = None
local._pdbpp_completing = False
local._pdbpp_in_init = False


def __getattr__(name):
    """Backward compatibility (Python 3.7+)"""
    if name == "GLOBAL_PDB":
        return local.GLOBAL_PDB
    raise AttributeError("module '{}' has no attribute '{}'".format(__name__, name))


def import_from_stdlib(name):
    import code  # arbitrary module which stays in the same dir as pdb
    result = types.ModuleType(name)

    stdlibdir, _ = os.path.split(code.__file__)
    pyfile = os.path.join(stdlibdir, name + '.py')
    with open(pyfile) as f:
        src = f.read()
    co_module = compile(src, pyfile, 'exec', dont_inherit=True)
    exec(co_module, result.__dict__)

    return result


pdb = import_from_stdlib('pdb')


def _newfunc(func, newglobals):
    newfunc = types.FunctionType(func.__code__, newglobals, func.__name__,
                                 func.__defaults__, func.__closure__)
    if sys.version_info >= (3, ):
        newfunc.__annotations__ = func.__annotations__
        newfunc.__kwdefaults__ = func.__kwdefaults__
    return newfunc


def rebind_globals(func, newglobals):
    if hasattr(func, "__code__"):
        return _newfunc(func, newglobals)

    import functools

    if isinstance(func, functools.partial):
        return functools.partial(
            _newfunc(func.func, newglobals), *func.args, **func.keywords
        )

    raise ValueError("cannot handle func {!r}".format(func))


class DefaultConfig(object):
    prompt = '(Pdb++) '
    highlight = True
    sticky_by_default = False

    # Pygments.
    use_pygments = None  # Tries to use it if available.
    pygments_formatter_class = None  # Defaults to autodetect, based on $TERM.
    pygments_formatter_kwargs = {}
    # Legacy options.  Should use pygments_formatter_kwargs instead.
    bg = 'dark'
    colorscheme = None

    editor = None  # Autodetected if unset.
    stdin_paste = None       # for emacs, you can use my bin/epaste script
    truncate_long_lines = True
    exec_if_unfocused = None
    disable_pytest_capturing = False
    encodings = ('utf-8', 'latin-1')

    enable_hidden_frames = True
    show_hidden_frames_count = True

    line_number_color = Color.turquoise
    filename_color = Color.yellow
    current_line_color = "39;49;7"  # default fg, bg, inversed

    show_traceback_on_error = True
    show_traceback_on_error_limit = None

    # Default keyword arguments passed to ``Pdb`` constructor.
    default_pdb_kwargs = {
    }

    def setup(self, pdb):
        pass

    def before_interaction_hook(self, pdb):
        pass


def setbgcolor(line, color):
    # hack hack hack
    # add a bgcolor attribute to all escape sequences found
    import re
    setbg = '\x1b[%sm' % color
    regexbg = '\\1;%sm' % color
    result = setbg + re.sub('(\x1b\\[.*?)m', regexbg, line) + '\x1b[00m'
    if os.environ.get('TERM') == 'eterm-color':
        # it seems that emacs' terminal has problems with some ANSI escape
        # sequences. Eg, 'ESC[44m' sets the background color in all terminals
        # I tried, but not in emacs. To set the background color, it needs to
        # have also an explicit foreground color, e.g. 'ESC[37;44m'. These
        # three lines are a hack, they try to add a foreground color to all
        # escape sequences which are not recognized by emacs. However, we need
        # to pick one specific fg color: I choose white (==37), but you might
        # want to change it.  These lines seems to work fine with the ANSI
        # codes produced by pygments, but they are surely not a general
        # solution.
        result = result.replace(setbg, '\x1b[37;%dm' % color)
        result = result.replace('\x1b[00;%dm' % color, '\x1b[37;%dm' % color)
        result = result.replace('\x1b[39;49;00;', '\x1b[37;')
    return result


CLEARSCREEN = '\033[2J\033[1;1H'


def lasti2lineno(code, lasti):
    import dis
    linestarts = list(dis.findlinestarts(code))
    linestarts.reverse()
    for i, lineno in linestarts:
        if lasti >= i:
            return lineno
    return 0


class Undefined:
    def __repr__(self):
        return '<undefined>'


undefined = Undefined()


class ArgWithCount(str):
    """Extend arguments with a count, e.g. "10pp …"."""
    def __new__(cls, value, count, **kwargs):
        obj = super(ArgWithCount, cls).__new__(cls, value)
        obj.cmd_count = count
        return obj

    def __repr__(self):
        return "<{} cmd_count={!r} value={}>".format(
            self.__class__.__name__,
            self.cmd_count,
            super(ArgWithCount, self).__repr__(),
        )


class PdbMeta(type):
    def __call__(cls, *args, **kwargs):
        """Reuse an existing instance with ``pdb.set_trace()``."""

        # Prevent recursion errors with pdb.set_trace() during init/debugging.
        if getattr(local, "_pdbpp_in_init", False):
            class OrigPdb(pdb.Pdb, object):
                def set_trace(self, frame=None):
                    print("pdb++: using pdb.Pdb for recursive set_trace.")
                    if frame is None:
                        frame = sys._getframe().f_back
                    super(OrigPdb, self).set_trace(frame)
            orig_pdb = OrigPdb.__new__(OrigPdb)
            # Remove any pdb++ only kwargs.
            kwargs.pop("Config", None)
            orig_pdb.__init__(*args, **kwargs)
            local._pdbpp_in_init = False
            return orig_pdb
        local._pdbpp_in_init = True

        global_pdb = getattr(local, "GLOBAL_PDB", None)
        if global_pdb:
            use_global_pdb = kwargs.pop(
                "use_global_pdb",
                (
                    not global_pdb._in_interaction
                    and os.environ.get("PDBPP_REUSE_GLOBAL_PDB", "1") == "1"
                ),
            )
        else:
            use_global_pdb = kwargs.pop("use_global_pdb", True)

        frame = sys._getframe().f_back
        called_for_set_trace = PdbMeta.called_for_set_trace(frame)
        if (
            use_global_pdb
            and global_pdb
            and called_for_set_trace
            and (
                hasattr(global_pdb, "_force_use_as_global_pdb")
                or cls.use_global_pdb_for_class(global_pdb, cls)
            )
        ):
            if hasattr(global_pdb, "botframe"):
                # Do not stop while tracing is active (in _set_stopinfo).
                # But skip it with instances that have not called set_trace
                # before.
                # Excplicitly unset tracing function always (with breakpoints).
                sys.settrace(None)
                global_pdb.set_continue()
                global_pdb._set_trace_use_next = True

            stdout = kwargs.get("stdout", sys.stdout)
            global_pdb._setup_streams(stdout=stdout)

            local._pdbpp_in_init = False
            return global_pdb

        obj = cls.__new__(cls)
        if called_for_set_trace:
            kwargs.setdefault("start_filename", called_for_set_trace.f_code.co_filename)
            kwargs.setdefault("start_lineno", called_for_set_trace.f_lineno)

        if "set_global_pdb" in kwargs:
            set_global_pdb = kwargs.pop("set_global_pdb", use_global_pdb)
            if set_global_pdb:
                obj._force_use_as_global_pdb = True
        else:
            set_global_pdb = use_global_pdb
        obj.__init__(*args, **kwargs)
        if set_global_pdb:
            obj._env = {"HOME": os.environ.get("HOME")}
            local.GLOBAL_PDB = obj
        local._pdbpp_in_init = False
        return obj

    @classmethod
    def use_global_pdb_for_class(cls, obj, C):
        _env = getattr(obj, "_env", None)
        if _env is not None and _env.get("HOME") != os.environ.get("HOME"):
            return False
        if type(obj) == C:
            return True
        if getattr(obj, "_use_global_pdb_for_class", None) == C:
            return True
        if sys.version_info < (3, 3):
            return inspect.getsourcelines(obj.__class__) == inspect.getsourcelines(C)
        return C.__qualname__ == obj.__class__.__qualname__

    @staticmethod
    def called_for_set_trace(frame):
        called_for_set_trace = False
        while frame:
            if (
                frame.f_code.co_name == "set_trace"
                and frame.f_back
                and "set_trace"
                in (frame.f_back.f_code.co_names + frame.f_back.f_code.co_varnames)
            ):
                called_for_set_trace = frame
                break
            frame = frame.f_back
        return called_for_set_trace


@six.add_metaclass(PdbMeta)
class Pdb(pdb.Pdb, ConfigurableClass, object):

    DefaultConfig = DefaultConfig
    config_filename = '.pdbrc.py'
    disabled = False
    fancycompleter = None

    _in_interaction = False

    def __init__(self, *args, **kwds):
        self.ConfigFactory = kwds.pop('Config', None)
        self.start_lineno = kwds.pop('start_lineno', None)
        self.start_filename = kwds.pop('start_filename', None)

        self.config = self.get_config(self.ConfigFactory)
        self.config.setup(self)

        if "PDBPP_COLORS" in os.environ:
            use_colors = bool(int(os.environ["PDBPP_COLORS"]))
            self.config.highlight = self.config.use_pygments = use_colors

        if self.config.disable_pytest_capturing:
            self._disable_pytest_capture_maybe()
        kwargs = self.config.default_pdb_kwargs.copy()
        kwargs.update(**kwds)
        super(Pdb, self).__init__(*args, **kwargs)
        self.prompt = self.config.prompt
        self.display_list = {}  # frame --> (name --> last seen value)
        self.tb_lineno = {}  # frame --> lineno where the exception raised
        self.history = []
        self.show_hidden_frames = False
        self.hidden_frames = []

        # Sticky mode.
        self.sticky = self.config.sticky_by_default
        self.first_time_sticky = self.sticky
        self.sticky_ranges = {}  # frame --> (start, end)
        self._sticky_messages = []  # Message queue for sticky mode.
        self._sticky_need_cls = False
        self._sticky_skip_cls = False

        self._setup_streams(stdout=self.stdout)

    @property
    def prompt(self):
        return self._prompt

    @prompt.setter
    def prompt(self, value):
        """Ensure there is "++" in the prompt always."""
        if "++" not in value:
            m = re.match(r"^(.*\w)(\s*\W\s*)?$", value)
            if m:
                value = "{}++{}".format(*m.groups(""))
        self._prompt = value

    def _setup_streams(self, stdout):
        self.stdout = self.ensure_file_can_write_unicode(stdout)

    def ensure_file_can_write_unicode(self, f):
        # Wrap with an encoder, but only if not already wrapped
        if (not hasattr(f, 'stream')
                and getattr(f, 'encoding', False)
                and f.encoding.lower() != 'utf-8'):
            f = codecs.getwriter('utf-8')(getattr(f, 'buffer', f))

        return f

    def _disable_pytest_capture_maybe(self):
        try:
            import py.test
            # Force raising of ImportError if pytest is not installed.
            py.test.config
        except (ImportError, AttributeError):
            return
        try:
            capman = py.test.config.pluginmanager.getplugin('capturemanager')
            capman.suspendcapture()
        except KeyError:
            pass
        except AttributeError:
            # Newer pytest with support ready, or very old py.test for which
            # this hack does not work.
            pass

    def _install_linecache_wrapper(self):
        """Disable linecache.checkcache to not invalidate caches.

        This gets installed permanently to also bypass e.g. pytest using
        `inspect.getsource`, which would invalidate it outside of the
        interaction them.
        """
        if not hasattr(self, "_orig_linecache_checkcache"):
            import linecache

            # Save it, although not used really (can be useful for debugging).
            self._orig_linecache_checkcache = linecache.checkcache

            def _linecache_checkcache(*args, **kwargs):
                return

            linecache.checkcache = _linecache_checkcache

    def interaction(self, frame, traceback):
        self._install_linecache_wrapper()

        self._in_interaction = True
        try:
            return self._interaction(frame, traceback)
        finally:
            self._in_interaction = False

    def _interaction(self, frame, traceback):
        # Restore the previous signal handler at the Pdb prompt.
        if getattr(pdb.Pdb, '_previous_sigint_handler', None):
            try:
                signal.signal(signal.SIGINT, pdb.Pdb._previous_sigint_handler)
            except ValueError:  # ValueError: signal only works in main thread
                pass
            else:
                pdb.Pdb._previous_sigint_handler = None
        ret = self.setup(frame, traceback)
        if ret:
            # no interaction desired at this time (happens if .pdbrc contains
            # a command like "continue")
            self.forget()
            return
        if self.config.exec_if_unfocused:
            self.exec_if_unfocused()

        # Handle post mortem via main: add exception similar to user_exception.
        if frame is None and traceback:
            exc = sys.exc_info()[:2]
            if exc != (None, None):
                self.curframe.f_locals['__exception__'] = exc

        if not self.sticky:
            self.print_stack_entry(self.stack[self.curindex])
            self.print_hidden_frames_count()

        with self._custom_completer():
            self.config.before_interaction_hook(self)
            # Use _cmdloop on py3 which catches KeyboardInterrupt.
            if hasattr(self, '_cmdloop'):
                self._cmdloop()
            else:
                self.cmdloop()

        self.forget()

    def _sticky_handle_cls(self):
        if self._sticky_skip_cls:
            self._sticky_skip_cls = False
            return
        if not self._sticky_need_cls:
            return

        self.stdout.write(CLEARSCREEN)
        self.stdout.flush()
        self._sticky_need_cls = False

    def postcmd(self, stop, line):
        """Handle clearing of the screen for sticky mode."""
        stop = super(Pdb, self).postcmd(stop, line)
        if self.sticky:
            if stop:
                self._sticky_handle_cls()
            else:
                if self._sticky_messages:
                    for msg in self._sticky_messages:
                        print(msg, file=self.stdout)
                    self._sticky_messages = []
                self._sticky_last_frame = self.stack[self.curindex]
        return stop

    def set_continue(self):
        if self.sticky:
            self._sticky_skip_cls = True
        super(Pdb, self).set_continue()

    def set_quit(self):
        if self.sticky:
            self._sticky_skip_cls = True
        super(Pdb, self).set_quit()

    def _setup_fancycompleter(self):
        """Similar to fancycompleter.setup(), but returning the old completer."""
        if not self.fancycompleter:
            self.fancycompleter = Completer(namespace={})
        completer = self.fancycompleter
        readline = completer.config.readline
        old_completer = readline.get_completer()
        if fancycompleter.has_leopard_libedit(completer.config):
            readline.parse_and_bind("bind ^I rl_complete")
        else:
            readline.parse_and_bind('tab: complete')
        readline.set_completer(self.complete)
        return old_completer

    @contextlib.contextmanager
    def _custom_completer(self):
        old_completer = self._setup_fancycompleter()

        self._lastcompstate = [None, 0]
        try:
            yield
        finally:
            self.fancycompleter.config.readline.set_completer(old_completer)

    def print_hidden_frames_count(self):
        n = len(self.hidden_frames)
        if n and self.config.show_hidden_frames_count:
            plural = n > 1 and "s" or ""
            print(
                "   %d frame%s hidden (try 'help hidden_frames')" % (n, plural),
                file=self.stdout,
            )

    def exec_if_unfocused(self):
        import os
        import wmctrl
        term = os.getenv('TERM', '')
        try:
            winid = int(os.getenv('WINDOWID'))
        except (TypeError, ValueError):
            return  # cannot find WINDOWID of the terminal
        active_win = wmctrl.Window.get_active()
        if not active_win or (int(active_win.id, 16) != winid) and \
           not (active_win.wm_class == 'emacs.Emacs' and term.startswith('eterm')):
            os.system(self.config.exec_if_unfocused)

    def setup(self, frame, tb):
        ret = super(Pdb, self).setup(frame, tb)
        if not ret:
            while tb:
                lineno = lasti2lineno(tb.tb_frame.f_code, tb.tb_lasti)
                self.tb_lineno[tb.tb_frame] = lineno
                tb = tb.tb_next
        return ret

    def _is_hidden(self, frame):
        if not self.config.enable_hidden_frames:
            return False

        # Decorated code is always considered to be hidden.
        consts = frame.f_code.co_consts
        if consts and consts[-1] is _HIDE_FRAME:
            return True

        # Do not hide if this frame contains the initial set_trace.
        if frame is getattr(self, "_via_set_trace_frame", None):
            return False

        if frame.f_globals.get('__unittest'):
            return True
        if frame.f_locals.get('__tracebackhide__') \
           or frame.f_globals.get('__tracebackhide__'):
            return True

    def get_stack(self, f, t):
        # show all the frames, except the ones that explicitly ask to be hidden
        fullstack, idx = super(Pdb, self).get_stack(f, t)
        self.fullstack = fullstack
        return self.compute_stack(fullstack, idx)

    def compute_stack(self, fullstack, idx=None):
        if not fullstack:
            return fullstack, idx if idx is not None else 0
        if idx is None:
            idx = len(fullstack) - 1
        if self.show_hidden_frames:
            return fullstack, idx

        self.hidden_frames = []
        newstack = []
        for frame, lineno in fullstack:
            if self._is_hidden(frame):
                self.hidden_frames.append((frame, lineno))
            else:
                newstack.append((frame, lineno))
        if not newstack:
            newstack.append(self.hidden_frames.pop())
        newidx = idx - len(self.hidden_frames)
        return newstack, newidx

    def refresh_stack(self):
        """
        Recompute the stack after e.g. show_hidden_frames has been modified
        """
        self.stack, _ = self.compute_stack(self.fullstack)
        # find the current frame in the new stack
        for i, (frame, _) in enumerate(self.stack):
            if frame is self.curframe:
                self.curindex = i
                break
        else:
            self.curindex = len(self.stack)-1
            self.curframe = self.stack[-1][0]
            self.print_current_stack_entry()

    def reset(self):
        """Set values of attributes as ready to start debugging.

        This overrides Bdb.reset to not clear the linecache (bpo-39967)."""
        self.botframe = None
        self._set_stopinfo(None, None)
        self.forget()

    def forget(self):
        if not getattr(local, "_pdbpp_completing", False):
            super(Pdb, self).forget()

    @classmethod
    def _get_all_completions(cls, complete, text):
        r = []
        i = 0
        while True:
            comp = complete(text, i)
            if comp is None:
                break
            i += 1
            r.append(comp)
        return r

    @contextlib.contextmanager
    def _patch_readline_for_pyrepl(self):
        """Patch readline module used in original Pdb.complete."""
        uses_pyrepl = self.fancycompleter.config.readline != sys.modules["readline"]

        if not uses_pyrepl:
            yield
            return

        # Make pdb.Pdb.complete use pyrepl's readline.
        orig_readline = sys.modules["readline"]
        sys.modules["readline"] = self.fancycompleter.config.readline
        try:
            yield
        finally:
            sys.modules["readline"] = orig_readline

    def complete(self, text, state):
        try:
            return self._complete(text, state)
        except Exception as exc:
            self.error("error during completion: {}".format(exc))
            if self.config.show_traceback_on_error:
                __import__("traceback").print_exc(file=self.stdout)

    def _complete(self, text, state):
        """Handle completions from fancycompleter and original pdb."""
        if state == 0:
            local._pdbpp_completing = True

            self._completions = []

            # Get completions from fancycompleter.
            mydict = self.curframe.f_globals.copy()
            mydict.update(self.curframe_locals)
            completer = Completer(mydict)
            completions = self._get_all_completions(completer.complete, text)

            if self.fancycompleter.config.use_colors:
                clean_fancy_completions = set([
                    RE_REMOVE_FANCYCOMPLETER_ESCAPE_SEQS.sub("", x)
                    for x in completions
                ])
            else:
                clean_fancy_completions = completions

            # Get completions from original pdb.
            pdb_completions = []
            with self._patch_readline_for_pyrepl():
                real_pdb = super(Pdb, self)
                for x in self._get_all_completions(real_pdb.complete, text):
                    if x not in clean_fancy_completions:
                        pdb_completions.append(x)

            # Ignore "\t" as only completion from fancycompleter, if there are
            # pdb completions.
            if completions == ["\t"] and pdb_completions:
                completions = []

            self._completions = completions
            if pdb_completions:
                pdb_prefix = fancycompleter.commonprefix(pdb_completions)
                if '.' in text and pdb_prefix and len(pdb_completions) > 1:
                    # Remove prefix for attr_matches from pdb completions.
                    dotted = text.split('.')
                    prefix = '.'.join(dotted[:-1]) + '.'
                    prefix_len = len(prefix)
                    pdb_completions = [
                        x[prefix_len:] if x.startswith(prefix) else x
                        for x in pdb_completions
                    ]
                if len(completions) == 1 and "." in completions[0]:
                    if pdb_prefix:
                        pdb_completions = []

                for x in pdb_completions:
                    if x not in clean_fancy_completions:
                        self._completions.append(x)
            else:
                self._completions = completions

            self._filter_completions(text)

            local._pdbpp_completing = False

        try:
            return self._completions[state]
        except IndexError:
            return None

    def _filter_completions(self, text):
        # Remove anything prefixed with "_" / "__" by default, but only
        # display it on additional request (3rd tab, after pyrepl's "[not
        # unique]"), or if the prefix is used already.
        if text == self._lastcompstate[0]:
            if self._lastcompstate[1] > 0:
                return
            self._lastcompstate[1] += 1
        else:
            self._lastcompstate[0] = text
            self._lastcompstate[1] = 0

        if text[-1:] != "_":
            self._completions = [
                x
                for x in self._completions
                if RE_COLOR_ESCAPES.sub("", x)[:1] != "_"
            ]
        elif text[-2:] != "__":
            self._completions = [
                x
                for x in self._completions
                if RE_COLOR_ESCAPES.sub("", x)[:2] != "__"
            ]

    stack_entry_regexp = re.compile(r'(.*?)\(([0-9]+?)\)(.*)', re.DOTALL)

    def format_stack_entry(self, frame_lineno, lprefix=': '):
        entry = super(Pdb, self).format_stack_entry(frame_lineno, lprefix)
        entry = self.try_to_decode(entry)
        if self.config.highlight:
            match = self.stack_entry_regexp.match(entry)
            if match:
                filename, lineno, other = match.groups()
                filename = Color.set(self.config.filename_color, filename)
                lineno = Color.set(self.config.line_number_color, lineno)
                entry = '%s(%s)%s' % (filename, lineno, other)
        if self.config.use_pygments is not False:
            loc, _, source = entry.rpartition(lprefix)
            if _:
                entry = loc + _ + self.format_source(source).rstrip()
        return entry

    def try_to_decode(self, s):
        for encoding in self.config.encodings:
            try:
                return s.decode(encoding)
            except (UnicodeDecodeError, AttributeError):
                pass
        return s

    def try_to_encode(self, s):
        for encoding in self.config.encodings:
            try:
                return s.encode(encoding)
            except (UnicodeDecodeError, AttributeError):
                pass
        return s

    def _get_source_highlight_function(self):
        try:
            import pygments
            import pygments.lexers
        except ImportError:
            return False

        try:
            pygments_formatter = self._get_pygments_formatter()
        except Exception as exc:
            self.message("pdb++: could not setup Pygments, disabling: {}".format(
                exc
            ))
            return False

        lexer = pygments.lexers.PythonLexer(stripnl=False)

        def syntax_highlight(src):
            return pygments.highlight(src, lexer, pygments_formatter)

        return syntax_highlight

    def _get_pygments_formatter(self):
        if hasattr(self.config, 'formatter'):
            # Deprecated, never documented.
            # Not optimal, since it involves creating the formatter in
            # the config already, although it might never be used.
            return self.config.formatter

        if self.config.pygments_formatter_class:
            from importlib import import_module

            def import_string(dotted_path):
                module_path, class_name = dotted_path.rsplit('.', 1)
                module = import_module(module_path)
                return getattr(module, class_name)

            Formatter = import_string(self.config.pygments_formatter_class)
        else:
            import pygments.formatters

            if getattr(self.config, "use_terminal256formatter", None) is not None:
                # Deprecated, never really documented (only changelog).
                if self.config.use_terminal256formatter:
                    Formatter = pygments.formatters.Terminal256Formatter
                else:
                    Formatter = pygments.formatters.TerminalFormatter
            else:
                term = os.environ.get("TERM", "")
                if term in ("xterm-kitty",):
                    Formatter = pygments.formatters.TerminalTrueColorFormatter
                elif "256color" in term:
                    Formatter = pygments.formatters.Terminal256Formatter
                else:
                    Formatter = pygments.formatters.TerminalFormatter

        formatter_kwargs = {
            # Only used by TerminalFormatter.
            "bg": self.config.bg,
            "colorscheme": self.config.colorscheme,
            "style": "default",
        }
        formatter_kwargs.update(self.config.pygments_formatter_kwargs)

        return Formatter(**formatter_kwargs)

    def format_source(self, src):
        if self.config.use_pygments is False:
            return src

        if not hasattr(self, "_highlight"):
            self._highlight = self._get_source_highlight_function()

            if self._highlight is False:
                if self.config.use_pygments is True:
                    self.message("Could not import pygments, disabling.")
                self.config.use_pygments = False
                return src

        return self._highlight_cached(src)

    @lru_cache(maxsize=64)
    def _highlight_cached(self, src):
        src = self.try_to_decode(src)
        return self._highlight(src)

    def _format_line(self, lineno, marker, line, lineno_width):
        lineno = ('%%%dd' % lineno_width) % lineno
        if self.config.highlight:
            lineno = Color.set(self.config.line_number_color, lineno)
        line = '%s  %2s %s' % (lineno, marker, line)
        return line

    def execRcLines(self):
        self._pdbpp_executing_rc_lines = True
        try:
            return super(Pdb, self).execRcLines()
        finally:
            del self._pdbpp_executing_rc_lines

    def parseline(self, line):
        if getattr(self, "_pdbpp_executing_rc_lines", False):
            return super(Pdb, self).parseline(line)

        if line.startswith('!!'):
            # Force the "standard" behaviour, i.e. first check for the
            # command, then for the variable name to display.
            line = line[2:]
            return super(Pdb, self).parseline(line)

        if line.endswith('?') and not line.startswith("!"):
            arg = line.split('?', 1)[0]
            if line.endswith('??'):
                cmd = "inspect_with_source"
            elif arg == '' or (
                hasattr(self, 'do_' + arg)
                and arg not in self.curframe.f_globals
                and arg not in self.curframe_locals
            ):
                cmd = "help"
            else:
                cmd = "inspect"
            return cmd, arg, line

        # pdb++ "smart command mode": don't execute commands if a variable
        # with the name exists in the current context;
        # This prevents pdb to quit if you type e.g. 'r[0]' by mystake.
        cmd, arg, newline = super(Pdb, self).parseline(line)

        if cmd:
            # prefixed strings.
            if (
                cmd in ("b", "f", "r", "u")
                and len(newline) > 1
                and (newline[1] == "'" or newline[1] == '"')
            ):
                cmd, arg, newline = None, None, line
            else:
                # Handle "count" prefix with commands, transferring it to "arg".
                m = re.match(r"(\d+)(\w+)", cmd)
                if m:
                    arg = ArgWithCount(arg, count=int(m.group(1)))
                    cmd = m.group(2)

                if hasattr(self, "do_" + cmd):
                    if (
                        self.curframe
                        and (
                            cmd in self.curframe.f_globals
                            or cmd in self.curframe_locals
                        )
                        and cmd + arg == line  # not for "debug ..." etc
                    ) or arg.startswith("="):
                        cmd, arg, newline = None, None, line
                    elif arg.startswith("(") and cmd in ("list", "next"):
                        # heuristic: handle "list(...", "next(..." etc as builtin.
                        cmd, arg, newline = None, None, line

        # Fix cmd to not be None when used in completions.
        # This would trigger a TypeError (instead of AttributeError) in
        # Cmd.complete (https://bugs.python.org/issue35270).
        if cmd is None:
            f = sys._getframe()
            while f.f_back:
                f = f.f_back
                if f.f_code.co_name == "complete":
                    cmd = ""
                    break

        return cmd, arg, newline

    def do_inspect(self, arg):
        """Inspect argument.  Used with `obj?`."""
        self._do_inspect(arg, with_source=False)

    def do_inspect_with_source(self, arg):
        """Inspect argument with source (if available).  Used with `obj??`."""
        self._do_inspect(arg, with_source=True)

    def _do_inspect(self, arg, with_source=False):
        try:
            obj = self._getval(arg)
        except Exception:
            return

        data = OrderedDict()
        data['Type'] = type(obj).__name__
        data['String Form'] = str(obj).strip()
        try:
            data['Length'] = str(len(obj))
        except TypeError:
            pass
        try:
            data['File'] = inspect.getabsfile(obj)
        except TypeError:
            pass
        else:
            try:
                data['File'] += ":" + str(obj.__code__.co_firstlineno)
            except AttributeError:
                pass

        if (isinstance(obj, type)
                and hasattr(obj, '__init__')
                and getattr(obj, '__module__') != '__builtin__'):
            # Class - show definition and docstring for constructor
            data['Docstring'] = inspect.getdoc(obj)
            data['Constructor information'] = ''
            try:
                data['  Definition'] = '%s%s' % (arg, signature(obj))
            except ValueError:
                pass
            data['  Docstring'] = inspect.getdoc(obj.__init__)
        else:
            try:
                data['Definition'] = '%s%s' % (arg, signature(obj))
            except (TypeError, ValueError):
                pass
            data['Docstring'] = inspect.getdoc(obj)

        for key, value in data.items():
            formatted_key = Color.set(Color.red, key + ':')
            if value is None:
                continue
            if value:
                first_line, _, lines = str(value).partition("\n")
                formatted_value = first_line
                if lines:
                    indent = " " * 16
                    formatted_value += "\n" + "\n".join(
                        indent + line
                        for line in lines.splitlines()
                    )
            else:
                formatted_value = ""
            self.stdout.write('%-28s %s\n' % (formatted_key, formatted_value))

        if with_source:
            self.stdout.write("%-28s" % Color.set(Color.red, "Source:"))
            _, lineno, lines = self._get_position_of_obj(obj, quiet=True)
            if lineno is None:
                self.stdout.write(" -\n")
            else:
                self.stdout.write("\n")
                self._print_lines_pdbpp(lines, lineno, print_markers=False)

    def default(self, line):
        """Patched version to fix namespace with list comprehensions.

        Fixes https://bugs.python.org/issue21161.
        """
        self.history.append(line)
        if line[:1] == '!':
            line = line[1:]
        locals = self.curframe_locals
        ns = self.curframe.f_globals.copy()
        ns.update(locals)
        try:
            code = compile(line + '\n', '<stdin>', 'single')
            save_stdout = sys.stdout
            save_stdin = sys.stdin
            save_displayhook = sys.displayhook
            try:
                sys.stdin = self.stdin
                sys.stdout = self.stdout
                sys.displayhook = self.displayhook
                exec(code, ns, locals)
            finally:
                sys.stdout = save_stdout
                sys.stdin = save_stdin
                sys.displayhook = save_displayhook
        except:
            exc_info = sys.exc_info()[:2]
            self.error(traceback.format_exception_only(*exc_info)[-1].strip())

    def do_help(self, arg):
        try:
            return super(Pdb, self).do_help(arg)
        except AttributeError:
            print("*** No help for '{command}'".format(command=arg),
                  file=self.stdout)
    do_help.__doc__ = pdb.Pdb.do_help.__doc__

    def help_hidden_frames(self):
        print("""\
Some frames might be marked as "hidden": by default, hidden frames are not
shown in the stack trace, and cannot be reached using ``up`` and ``down``.
You can use ``hf_unhide`` to tell pdb++ to ignore the hidden status (i.e., to
treat hidden frames as normal ones), and ``hf_hide`` to hide them again.
``hf_list`` prints a list of hidden frames.

Frames can be marked as hidden in the following ways:

- by using the ``@pdb.hideframe`` function decorator

- by having ``__tracebackhide__=True`` in the locals or the globals of the
  function (this is used by pytest)

- by having ``__unittest=True`` in the globals of the function (this hides
  unittest internal stuff)

- by providing a list of skip patterns to the Pdb class constructor.  This
  list defaults to ``skip=["importlib._bootstrap"]``.

Note that the initial frame where ``set_trace`` was called from is not hidden,
except for when using the function decorator.
""", file=self.stdout)

    def do_hf_unhide(self, arg):
        """
        {hf_show}
        unhide hidden frames, i.e. make it possible to ``up`` or ``down``
        there
        """
        self.show_hidden_frames = True
        self.refresh_stack()

    def do_hf_hide(self, arg):
        """
        {hf_hide}
        (re)hide hidden frames, if they have been unhidden by ``hf_unhide``
        """
        self.show_hidden_frames = False
        self.refresh_stack()

    def do_hf_list(self, arg):
        for frame_lineno in self.hidden_frames:
            print(self.format_stack_entry(frame_lineno, pdb.line_prefix),
                  file=self.stdout)

    def do_longlist(self, arg):
        """
        {longlist|ll}
        List source code for the current function.

        Differently than list, the whole function is displayed; the
        current line is marked with '->'.  In case of post-mortem
        debugging, the line which effectively raised the exception is
        marked with '>>'.

        If the 'highlight' config option is set and pygments is
        installed, the source code is colorized.
        """
        self.lastcmd = 'longlist'
        self._printlonglist(max_lines=False)

    do_ll = do_longlist

    def _printlonglist(self, linerange=None, max_lines=None):
        try:
            if self.curframe.f_code.co_name == '<module>':
                # inspect.getsourcelines is buggy in this case: if we just
                # pass the frame, it returns the source for the first function
                # defined in the module.  Instead, we want the full source
                # code of the module
                lines, _ = inspect.findsource(self.curframe)
                lineno = 1
            else:
                try:
                    lines, lineno = inspect.getsourcelines(self.curframe)
                except Exception as e:
                    print('** Error in inspect.getsourcelines: %s **' %
                          e, file=self.stdout)
                    return
        except IOError as e:
            print('** Error: %s **' % e, file=self.stdout)
            return
        if linerange:
            start, end = linerange
            start = max(start, lineno)
            end = min(end, lineno+len(lines))
            lines = lines[start-lineno:end-lineno]
            lineno = start
        self._print_lines_pdbpp(lines, lineno, max_lines=max_lines)

    @staticmethod
    def _truncate_to_visible_length(s, maxlength):
        """Truncate string to visible length (with escape sequences ignored)."""
        matches = list(RE_COLOR_ESCAPES.finditer(s))
        if not matches:
            return s[:maxlength]

        ret = ""
        total_visible_len = 0
        pos = 0
        for m in matches:
            m_start = m.regs[0][0]
            m_end = m.regs[0][1]
            add_visible = s[pos:m_start]
            len_visible = m_start - pos
            overflow = (len_visible + total_visible_len) - maxlength
            if overflow >= 0:
                if overflow == 0:
                    ret += add_visible
                else:
                    ret += add_visible[:-overflow]
                ret += s[m_start:m_end]
                break
            total_visible_len += len_visible
            ret += add_visible
            ret += s[m_start:m_end]
            pos = m_end
        else:
            assert maxlength - total_visible_len > 0
            rest = s[m_end:]
            ret += rest[:maxlength - total_visible_len]

        # Keep reset sequence (in last match).
        if len(ret) != len(s):
            last_m_start, last_m_end = matches[-1].span()
            if last_m_end == len(s):
                reset_seq = s[last_m_start:last_m_end]
                if not ret.endswith(reset_seq):
                    ret += reset_seq

        assert len(RE_COLOR_ESCAPES.sub("", ret)) <= maxlength
        return ret

    def _cut_lines(self, lines, lineno, max_lines):
        max_lines = max(6, len(lines) if not max_lines else max_lines)
        if len(lines) <= max_lines:
            for i, line in enumerate(lines, lineno):
                yield i, line
            return

        cutoff = len(lines) - max_lines

        # Keep certain top lines.
        # Keeps decorators, but not functions, which are displayed at the top
        # already (stack information).
        # TODO: check behavior with lambdas.
        COLOR_OR_SPACE = r'(?:\x1b[^m]+m|\s)'
        keep_pat = re.compile(
            r'(?:^{col}*@)'
            r'|(?<!\w)lambda(?::|{col})'.format(col=COLOR_OR_SPACE)
        )
        keep_head = 0
        while keep_pat.match(lines[keep_head]):
            keep_head += 1

        if keep_head > 3:
            yield lineno, lines[0]
            yield None, '...'
            yield lineno + keep_head, lines[keep_head-1]
            cutoff -= keep_head - 3
        else:
            for i, line in enumerate(lines[:keep_head]):
                yield lineno + i, line

        exc_lineno = self.tb_lineno.get(self.curframe, None)
        last_marker_line = max(
            self.curframe.f_lineno,
            exc_lineno if exc_lineno else 0) - lineno

        # Place marker / current line in first third of available lines.
        cut_before = min(
            cutoff, max(0, last_marker_line - max_lines + max_lines // 3 * 2)
        )
        cut_after = cutoff - cut_before

        # Adjust for '...' lines.
        cut_after = cut_after + 1 if cut_after > 0 else 0
        if cut_before:
            # Adjust for '...' line.
            cut_before += 1

        for i, line in enumerate(lines[keep_head:], keep_head):
            if cut_before:
                cut_before -= 1
                if cut_before == 0:
                    yield None, '...'
                else:
                    assert cut_before > 0, cut_before
                continue
            elif cut_after and i >= len(lines) - cut_after:
                yield None, '...'
                break
            yield lineno + i, line

    def _print_lines_pdbpp(self, lines, lineno, print_markers=True, max_lines=None):
        lines = [line[:-1] for line in lines]  # remove the trailing '\n'
        lines = [line.replace('\t', '    ')
                 for line in lines]  # force tabs to 4 spaces
        width, height = self.get_terminal_size()

        if self.config.use_pygments is not False:
            src = self.format_source('\n'.join(lines))
            lines = src.splitlines()

        if self.config.truncate_long_lines:
            maxlength = max(width - 9, 16)
            lines = [self._truncate_to_visible_length(line, maxlength)
                     for line in lines]

        lineno_width = len(str(lineno + len(lines)))
        exc_lineno = self.tb_lineno.get(self.curframe, None)

        new_lines = []
        if print_markers:
            set_bg = self.config.highlight and self.config.current_line_color
            for lineno, line in self._cut_lines(lines, lineno, max_lines):
                if lineno is None:
                    new_lines.append(line)
                    continue

                if lineno == self.curframe.f_lineno:
                    marker = '->'
                elif lineno == exc_lineno:
                    marker = '>>'
                else:
                    marker = ''
                line = self._format_line(lineno, marker, line, lineno_width)

                if marker == "->" and set_bg:
                    len_visible = len(RE_COLOR_ESCAPES.sub("", line))
                    line = line + " " * (width - len_visible)
                    line = setbgcolor(line, self.config.current_line_color)
                new_lines.append(line)
        else:
            for i, line in enumerate(lines):
                new_lines.append(self._format_line(lineno, '', line, lineno_width))
                lineno += 1
        print('\n'.join(new_lines), file=self.stdout)

    def _format_color_prefixes(self, lines):
        if not lines:
            return lines

        if self.config.use_pygments is False and not self.config.highlight:
            return lines

        # Format source without prefixes added by pdb, including line numbers.
        prefixes = []
        src_lines = []

        for x in lines:
            prefix, _, src = x.partition('\t')
            prefixes.append(prefix)
            src_lines.append(src)

        RE_LNUM_PREFIX = re.compile(r"^\d+")
        if self.config.highlight:
            prefixes = [
                RE_LNUM_PREFIX.sub(
                    lambda m: Color.set(self.config.line_number_color, m.group(0)),
                    prefix
                )
                for prefix in prefixes
            ]

        return [
            "%s\t%s" % (prefix, src)
            for (prefix, src) in zip(prefixes, src_lines)
        ]

    @contextlib.contextmanager
    def _patch_linecache_for_source_highlight(self):
        orig = pdb.linecache.getlines

        def wrapped_getlines(filename, globals):
            """Wrap linecache.getlines to highlight source (for do_list)."""
            old_cache = pdb.linecache.cache.pop(filename, None)
            try:
                lines = orig(filename, globals)
            finally:
                if old_cache:
                    pdb.linecache.cache[filename] = old_cache
            source = self.format_source("".join(lines))

            if sys.version_info < (3,):
                source = self.try_to_encode(source)

            return source.splitlines(True)

        pdb.linecache.getlines = wrapped_getlines

        try:
            yield
        finally:
            pdb.linecache.getlines = orig

    def do_list(self, arg):
        """Enhance original do_list with highlighting."""
        if not (self.config.use_pygments is not False or self.config.highlight):
            return super(Pdb, self).do_list(arg)

        with self._patch_linecache_for_source_highlight():

            oldstdout = self.stdout
            self.stdout = StringIO()
            ret = super(Pdb, self).do_list(arg)
            orig_pdb_lines = self.stdout.getvalue().splitlines()
            self.stdout = oldstdout

        for line in self._format_color_prefixes(orig_pdb_lines):
            print(line, file=self.stdout)
        return ret

    do_list.__doc__ = pdb.Pdb.do_list.__doc__
    do_l = do_list

    def _select_frame(self, number):
        """Same as pdb.Pdb, but uses print_current_stack_entry (for sticky)."""
        assert 0 <= number < len(self.stack), (number, len(self.stack))
        self.curindex = number
        self.curframe = self.stack[self.curindex][0]
        self.curframe_locals = self.curframe.f_locals
        self.print_current_stack_entry()
        self.lineno = None

    def do_continue(self, arg):
        if arg != '':
            self._seen_error = False
            self.do_tbreak(arg)
            if self._seen_error:
                return 0
        return super(Pdb, self).do_continue('')
    do_continue.__doc__ = pdb.Pdb.do_continue.__doc__
    do_c = do_cont = do_continue

    def do_p(self, arg):
        """p expression
        Print the value of the expression.
        """
        try:
            val = self._getval(arg)
        except:
            return
        try:
            self.message(repr(val))
        except:
            exc_info = sys.exc_info()[:2]
            self.error(traceback.format_exception_only(*exc_info)[-1].strip())

    def do_pp(self, arg):
        """[width]pp expression
        Pretty-print the value of the expression.
        """
        width = getattr(arg, "cmd_count", None)
        try:
            val = self._getval(arg)
        except:
            return
        if width is None:
            try:
                width, _ = self.get_terminal_size()
            except Exception as exc:
                self.message("warning: could not get terminal size ({})".format(exc))
                width = None
        try:
            pprint.pprint(val, self.stdout, width=width)
        except:
            exc_info = sys.exc_info()[:2]
            self.error(traceback.format_exception_only(*exc_info)[-1].strip())
    do_pp.__doc__ = pdb.Pdb.do_pp.__doc__

    def do_debug(self, arg):
        # this is a hack (as usual :-))
        #
        # inside the original do_debug, there is a call to the global "Pdb" to
        # instantiate the recursive debugger: we want to intercept this call
        # and instantiate *our* Pdb, passing our custom config. Therefore we
        # dynamically rebind the globals.
        Config = self.ConfigFactory

        class PdbppWithConfig(self.__class__):
            def __init__(self_withcfg, *args, **kwargs):
                kwargs.setdefault("Config", Config)
                super(PdbppWithConfig, self_withcfg).__init__(*args, **kwargs)

                # Backport of fix for bpo-31078 (not yet merged).
                self_withcfg.use_rawinput = self.use_rawinput

                local.GLOBAL_PDB = self_withcfg
                local.GLOBAL_PDB._use_global_pdb_for_class = self.__class__

        if sys.version_info < (3, ):
            do_debug_func = pdb.Pdb.do_debug.im_func
        else:
            do_debug_func = pdb.Pdb.do_debug

        newglobals = do_debug_func.__globals__.copy()
        newglobals['Pdb'] = PdbppWithConfig
        new_do_debug = rebind_globals(do_debug_func, newglobals)

        # Handle any exception, e.g. SyntaxErrors.
        # This is about to be improved in Python itself (3.8, 3.7.3?).
        prev_pdb = local.GLOBAL_PDB
        try:
            with self._custom_completer():
                return new_do_debug(self, arg)
        except Exception:
            exc_info = sys.exc_info()[:2]
            msg = traceback.format_exception_only(*exc_info)[-1].strip()
            self.error(msg)
        finally:
            local.GLOBAL_PDB = prev_pdb

    do_debug.__doc__ = pdb.Pdb.do_debug.__doc__

    def do_interact(self, arg):
        """
        interact

        Start an interative interpreter whose global namespace
        contains all the names found in the current scope.
        """
        ns = self.curframe.f_globals.copy()
        ns.update(self.curframe_locals)
        code.interact("*interactive*", local=ns)

    def do_track(self, arg):
        """
        track expression

        Display a graph showing which objects are referred by the
        value of the expression.  This command requires pypy to be in
        the current PYTHONPATH.
        """
        try:
            from rpython.translator.tool.reftracker import track
        except ImportError:
            print('** cannot import pypy.translator.tool.reftracker **',
                  file=self.stdout)
            return
        try:
            val = self._getval(arg)
        except:
            pass
        else:
            track(val)

    def _get_display_list(self):
        return self.display_list.setdefault(self.curframe, {})

    def _getval_or_undefined(self, arg):
        try:
            return eval(arg, self.curframe.f_globals,
                        self.curframe_locals)
        except NameError:
            return undefined

    def do_display(self, arg):
        """
        display expression

        Add expression to the display list; expressions in this list
        are evaluated at each step, and printed every time its value
        changes.

        WARNING: since the expressions is evaluated multiple time, pay
        attention not to put expressions with side-effects in the
        display list.
        """
        try:
            value = self._getval_or_undefined(arg)
        except:
            return
        self._get_display_list()[arg] = value

    def do_undisplay(self, arg):
        """
        undisplay expression

        Remove expression from the display list.
        """
        try:
            del self._get_display_list()[arg]
        except KeyError:
            print('** %s not in the display list **' % arg, file=self.stdout)

    def _print_if_sticky(self):
        if self.sticky:
            self._sticky_handle_cls()
            width, height = self.get_terminal_size()

            frame, lineno = self.stack[self.curindex]
            stack_entry = self._get_formatted_stack_entry(
                self.stack[self.curindex], "__CUTOFF_MARKER__"
            )
            s = stack_entry.split("__CUTOFF_MARKER__")[0]  # hack
            top_lines = []
            if self._sticky_messages:
                for msg in self._sticky_messages:
                    if msg == "--Return--" and (
                        "__return__" in frame.f_locals
                        or "__exception__" in frame.f_locals
                    ):
                        # Handled below.
                        continue
                    if msg.startswith("--") and msg.endswith("--"):
                        s += ", {}".format(msg)
                    else:
                        top_lines.append(msg)
                self._sticky_messages = []

            if self.config.show_hidden_frames_count:
                n = len(self.hidden_frames)
                if n:
                    plural = n > 1 and "s" or ""
                    s += ", %d frame%s hidden" % (n, plural)
            top_lines.append(s)

            sticky_range = self.sticky_ranges.get(self.curframe, None)

            after_lines = []
            if '__exception__' in frame.f_locals:
                s = self._format_exc_for_sticky(frame.f_locals['__exception__'])
                if s:
                    after_lines.append(s)

            elif getattr(sys, "last_value", None):
                s = self._format_exc_for_sticky((type(sys.last_value), sys.last_value))
                if s:
                    after_lines.append(s)

            elif '__return__' in frame.f_locals:
                rv = frame.f_locals['__return__']
                try:
                    s = repr(rv)
                except KeyboardInterrupt:
                    raise
                except:
                    s = '(unprintable return value)'

                s = ' return ' + s
                if self.config.highlight:
                    s = Color.set(self.config.line_number_color, s)
                after_lines.append(s)

            top_extra_lines = 0
            for line in top_lines:
                print(line, file=self.stdout)
                len_visible = len(RE_COLOR_ESCAPES.sub("", line))
                top_extra_lines += (len_visible - 1) // width + 2
            print(file=self.stdout)

            # Arrange for prompt and extra lines on top (location + newline
            # typically), and keep an empty line at the end (after prompt), so
            # that any output shows up at the top.
            max_lines = height - top_extra_lines - len(after_lines) - 2

            self._printlonglist(sticky_range, max_lines=max_lines)

            for line in after_lines:
                print(line, file=self.stdout)
            self._sticky_need_cls = True

    def _format_exc_for_sticky(self, exc):
        if len(exc) != 2:
            return "pdbpp: got unexpected __exception__: %r" % (exc,)

        exc_type, exc_value = exc
        s = ''
        try:
            try:
                s = exc_type.__name__
            except AttributeError:
                s = str(exc_type)
            if exc_value is not None:
                s += ': '
                s += str(exc_value)
        except KeyboardInterrupt:
            raise
        except Exception as exc:
            try:
                s += '(unprintable exception: %r)' % (exc,)
            except:
                s += '(unprintable exception)'
        else:
            # Use first line only, limited to terminal width.
            s = s.replace("\r", r"\r").replace("\n", r"\n")
            width, _ = self.get_terminal_size()
            if len(s) > width:
                s = s[:width - 1] + "…"

        if self.config.highlight:
            s = Color.set(self.config.line_number_color, s)

        return s

    def do_sticky(self, arg):
        """
        sticky [start end]

        Toggle sticky mode. When in sticky mode, it clear the screen
        and longlist the current functions, making the source
        appearing always in the same position. Useful to follow the
        flow control of a function when doing step-by-step execution.

        If ``start`` and ``end`` are given, sticky mode is enabled and
        only lines within that range (extremes included) will be
        displayed (for the current frame).
        """
        was_sticky = self.sticky
        if arg:
            try:
                start, end = map(int, arg.split())
            except ValueError:
                print('** Error when parsing argument: %s **' % arg,
                      file=self.stdout)
                return
            self.sticky = True
            self.sticky_ranges[self.curframe] = start, end+1
        else:
            self.sticky = not self.sticky
            self.sticky_range = None
        if not was_sticky and self.sticky:
            self._sticky_need_cls = True
        self._print_if_sticky()

    def print_stack_trace(self):
        try:
            for frame_index, frame_lineno in enumerate(self.stack):
                self.print_stack_entry(frame_lineno, frame_index=frame_index)
        except KeyboardInterrupt:
            pass

    def print_stack_entry(
        self, frame_lineno, prompt_prefix=pdb.line_prefix, frame_index=None
    ):
        print(
            self._get_formatted_stack_entry(frame_lineno, prompt_prefix, frame_index),
            file=self.stdout,
        )

    def _get_formatted_stack_entry(
        self, frame_lineno, prompt_prefix=pdb.line_prefix, frame_index=None
    ):
        frame, lineno = frame_lineno
        if frame is self.curframe:
            marker = "> "
        else:
            marker = "  "

        frame_prefix_width = len(str(len(self.stack)))
        if frame_index is None:
            frame_index = self.curindex
            fmt = "{frame_prefix}{marker}"
            lprefix = prompt_prefix  # "\n ->" by default
        else:
            # via/for stack trace
            fmt = "{marker}{frame_prefix}"
            lprefix = "\n     " + (" " * frame_prefix_width)

        # Format stack index (keeping same width across stack).
        frame_prefix = ("[%%%dd] " % frame_prefix_width) % frame_index
        marker_frameno = fmt.format(marker=marker, frame_prefix=frame_prefix)

        return marker_frameno + self.format_stack_entry(frame_lineno, lprefix)

    def print_current_stack_entry(self):
        if self.sticky:
            self._print_if_sticky()
        else:
            self.print_stack_entry(self.stack[self.curindex])

    def preloop(self):
        self._print_if_sticky()

        display_list = self._get_display_list()
        for expr, oldvalue in display_list.items():
            newvalue = self._getval_or_undefined(expr)
            # check for identity first; this prevents custom __eq__ to
            # be called at every loop, and also prevents instances
            # whose fields are changed to be displayed
            if newvalue is not oldvalue or newvalue != oldvalue:
                display_list[expr] = newvalue
                print('%s: %r --> %r' % (expr, oldvalue, newvalue),
                      file=self.stdout)

    def _get_position_of_arg(self, arg):
        try:
            obj = self._getval(arg)
        except:
            return None, None, None
        return self._get_position_of_obj(obj)

    def _get_position_of_obj(self, obj, quiet=False):
        if hasattr(inspect, "unwrap"):
            obj = inspect.unwrap(obj)
        if isinstance(obj, str):
            return obj, 1, None
        try:
            filename = inspect.getabsfile(obj)
            lines, lineno = inspect.getsourcelines(obj)
        except (IOError, TypeError) as e:
            if not quiet:
                print('** Error: %s **' % e, file=self.stdout)
            return None, None, None
        return filename, lineno, lines

    def do_source(self, arg):
        _, lineno, lines = self._get_position_of_arg(arg)
        if lineno is None:
            return
        self._print_lines_pdbpp(lines, lineno, print_markers=False)

    def do_frame(self, arg):
        """f(rame) [index]
        Go to given frame.  The first frame is 0, negative index is counted
        from the end (i.e. -1 is the last one).
        Without argument, display current frame.
        """
        if not arg:
            # Just display the frame, without handling sticky.
            self.print_stack_entry(self.stack[self.curindex])
            return

        try:
            arg = int(arg)
        except (ValueError, TypeError):
            print(
                '*** Expected a number, got "{0}"'.format(arg), file=self.stdout)
            return
        if abs(arg) >= len(self.stack):
            print('*** Out of range', file=self.stdout)
            return
        if arg >= 0:
            self.curindex = arg
        else:
            self.curindex = len(self.stack) + arg
        self.curframe = self.stack[self.curindex][0]
        self.curframe_locals = self.curframe.f_locals
        self.print_current_stack_entry()
        self.lineno = None
    do_f = do_frame

    def do_up(self, arg='1'):
        arg = '1' if arg == '' else arg
        try:
            arg = int(arg)
        except (ValueError, TypeError):
            print(
                '*** Expected a number, got "{0}"'.format(arg), file=self.stdout)
            return
        if self.curindex - arg < 0:
            print('*** Oldest frame', file=self.stdout)
        else:
            self.curindex = self.curindex - arg
            self.curframe = self.stack[self.curindex][0]
            self.curframe_locals = self.curframe.f_locals
            self.print_current_stack_entry()
            self.lineno = None
    do_up.__doc__ = pdb.Pdb.do_up.__doc__
    do_u = do_up

    def do_down(self, arg='1'):
        arg = '1' if arg == '' else arg
        try:
            arg = int(arg)
        except (ValueError, TypeError):
            print(
                '*** Expected a number, got "{0}"'.format(arg), file=self.stdout)
            return
        if self.curindex + arg >= len(self.stack):
            print('*** Newest frame', file=self.stdout)
        else:
            self.curindex = self.curindex + arg
            self.curframe = self.stack[self.curindex][0]
            self.curframe_locals = self.curframe.f_locals
            self.print_current_stack_entry()
            self.lineno = None
    do_down.__doc__ = pdb.Pdb.do_down.__doc__
    do_d = do_down

    def do_top(self, arg):
        """Go to top (oldest) frame."""
        if self.curindex == 0:
            self.error('Oldest frame')
            return
        self._select_frame(0)
    do_top = do_top

    def do_bottom(self, arg):
        """Go to bottom (newest) frame."""
        if self.curindex + 1 == len(self.stack):
            self.error('Newest frame')
            return
        self._select_frame(len(self.stack) - 1)
    do_bottom = do_bottom

    @staticmethod
    def get_terminal_size():
        fallback = (80, 24)
        try:
            from shutil import get_terminal_size
        except ImportError:
            try:
                import termios
                import fcntl
                import struct
                call = fcntl.ioctl(0, termios.TIOCGWINSZ, "\x00"*8)
                height, width = struct.unpack("hhhh", call)[:2]
            except (SystemExit, KeyboardInterrupt):
                raise
            except:
                width = int(os.environ.get('COLUMNS', fallback[0]))
                height = int(os.environ.get('COLUMNS', fallback[1]))
            # Work around above returning width, height = 0, 0 in Emacs
            width = width if width != 0 else fallback[0]
            height = height if height != 0 else fallback[1]
            return width, height
        else:
            return get_terminal_size(fallback)

    def _open_editor(self, editcmd):
        """Extra method to allow for easy override in tests."""
        subprocess.Popen(editcmd, shell=True).communicate()

    def _get_current_position(self):
        frame = self.curframe
        lineno = frame.f_lineno
        filename = os.path.abspath(frame.f_code.co_filename)
        return filename, lineno

    def _quote_filename(self, filename):
        try:
            from shlex import quote
        except ImportError:
            from pipes import quote

        return quote(filename)

    def _format_editcmd(self, editor, filename, lineno):
        filename = self._quote_filename(filename)

        if "{filename}" in editor:
            return editor.format(filename=filename, lineno=lineno)

        if "%s" not in editor:
            # backward compatibility.
            return "%s +%d %s" % (editor, lineno, filename)

        # Replace %s with filename, %d with lineno; %% becomes %.
        return editor.replace("%%", "%").replace("%s", filename).replace(
            "%d", str(lineno))

    def _get_editor_cmd(self, filename, lineno):
        editor = self.config.editor
        if editor is None:
            try:
                editor = os.environ["EDITOR"]
            except KeyError:
                try:
                    from shutil import which
                except ImportError:
                    from distutils.spawn import find_executable as which
                editor = which("vim")
                if editor is None:
                    editor = which("vi")
            if not editor:
                raise RuntimeError("Could not detect editor. Configure it or set $EDITOR.")  # noqa: E501
        return self._format_editcmd(editor, filename, lineno)

    def do_edit(self, arg):
        "Open an editor visiting the current file at the current line"
        if arg == '':
            filename, lineno = self._get_current_position()
        else:
            filename, lineno, _ = self._get_position_of_arg(arg)
            if filename is None:
                return
        # this case handles code generated with py.code.Source()
        # filename is something like '<0-codegen foo.py:18>'
        match = re.match(r'.*<\d+-codegen (.*):(\d+)>', filename)
        if match:
            filename = match.group(1)
            lineno = int(match.group(2))

        try:
            self._open_editor(self._get_editor_cmd(filename, lineno))
        except Exception as exc:
            self.error(exc)

    do_ed = do_edit

    def _get_history(self):
        return [s for s in self.history if not side_effects_free.match(s)]

    def _get_history_text(self):
        import linecache
        line = linecache.getline(self.start_filename, self.start_lineno)
        nspaces = len(line) - len(line.lstrip())
        indent = ' ' * nspaces
        history = [indent + s for s in self._get_history()]
        return '\n'.join(history) + '\n'

    def _open_stdin_paste(self, stdin_paste, lineno, filename, text):
        proc = subprocess.Popen([stdin_paste, '+%d' % lineno, filename],
                                stdin=subprocess.PIPE)
        proc.stdin.write(text)
        proc.stdin.close()

    def _put(self, text):
        stdin_paste = self.config.stdin_paste
        if stdin_paste is None:
            print('** Error: the "stdin_paste" option is not configured **',
                  file=self.stdout)
        filename = self.start_filename
        lineno = self.start_lineno
        self._open_stdin_paste(stdin_paste, lineno, filename, text)

    def do_put(self, arg):
        text = self._get_history_text()
        self._put(text)

    def do_paste(self, arg):
        arg = arg.strip()
        old_stdout = self.stdout
        self.stdout = StringIO()
        self.onecmd(arg)
        text = self.stdout.getvalue()
        self.stdout = old_stdout
        sys.stdout.write(text)
        self._put(text)

    def set_step(self):
        """Use set_next() via set_trace() when re-using Pdb instance.

        But call set_step() before still for handling of frame_returning."""
        super(Pdb, self).set_step()
        if hasattr(self, "_set_trace_use_next"):
            del self._set_trace_use_next
            self.set_next(self._via_set_trace_frame)

    def stop_here(self, frame):
        # Always stop at starting frame (https://bugs.python.org/issue38806).
        if self.stopframe is None:
            if getattr(self, "_via_set_trace_frame", None) == frame:
                if not self._stopped_for_set_trace:
                    self._stopped_for_set_trace = True
                    return True
        if Pdb is not None:
            return super(Pdb, self).stop_here(frame)

    def set_trace(self, frame=None):
        """Remember starting frame.

        This is used with pytest, which does not use pdb.set_trace().
        """
        if getattr(local, "_pdbpp_completing", False):
            # Handle set_trace being called during completion, e.g. with
            # fancycompleter's attr_matches.
            return
        if self.disabled:
            return

        if frame is None:
            frame = sys._getframe().f_back
        self._via_set_trace_frame = frame
        self._stopped_for_set_trace = False

        self.start_filename = frame.f_code.co_filename
        self.start_lineno = frame.f_lineno

        return super(Pdb, self).set_trace(frame)

    def is_skipped_module(self, module_name):
        """Backport for https://bugs.python.org/issue36130.

        Fixed in Python 3.8+.
        """
        if module_name is None:
            return False
        return super(Pdb, self).is_skipped_module(module_name)

    def message(self, msg):
        if self.sticky:
            if sys._getframe().f_back.f_code.co_name == "user_exception":
                # Exceptions are handled in sticky mode explicitly.
                return
            self._sticky_messages.append(msg)
            return
        print(msg, file=self.stdout)

    def error(self, msg):
        """Override/enhance default error method to display tracebacks."""
        self._seen_error = msg
        print("***", msg, file=self.stdout)

        if not self.config.show_traceback_on_error:
            return

        etype, evalue, tb = sys.exc_info()
        if tb and tb.tb_frame.f_code.co_name == "default":
            tb = tb.tb_next
            if tb and tb.tb_frame.f_code.co_filename == "<stdin>":
                tb = tb.tb_next
                if tb:  # only display with actual traceback.
                    self._remove_bdb_context(evalue)
                    tb_limit = self.config.show_traceback_on_error_limit
                    fmt_exc = traceback.format_exception(
                        etype, evalue, tb, limit=tb_limit
                    )

                    # Remove last line (exception string again).
                    if len(fmt_exc) > 1 and fmt_exc[-1][0] != " ":
                        fmt_exc.pop()

                    print("".join(fmt_exc).rstrip(), file=self.stdout)

    @staticmethod
    def _remove_bdb_context(evalue):
        """Remove exception context from Pdb from the exception.

        E.g. "AttributeError: 'Pdb' object has no attribute 'do_foo'",
        when trying to look up commands (bpo-36494).
        Only done for Python 3+.
        """
        if not hasattr(evalue, "__context__"):
            return

        removed_bdb_context = evalue
        while removed_bdb_context.__context__:
            ctx = removed_bdb_context.__context__
            if (
                isinstance(ctx, AttributeError)
                and ctx.__traceback__.tb_frame.f_code.co_name == "onecmd"
            ):
                removed_bdb_context.__context__ = None
                break
            removed_bdb_context = removed_bdb_context.__context__


# simplified interface

if hasattr(pdb, 'Restart'):
    Restart = pdb.Restart

if hasattr(pdb, '_usage'):
    _usage = pdb._usage

# copy some functions from pdb.py, but rebind the global dictionary
for name in 'run runeval runctx runcall main set_trace'.split():
    func = getattr(pdb, name)
    globals()[name] = rebind_globals(func, globals())
del name, func


# Post-Mortem interface

def post_mortem(t=None, Pdb=Pdb):
    # handling the default
    if t is None:
        # sys.exc_info() returns (type, value, traceback) if an exception is
        # being handled, otherwise it returns None
        t = sys.exc_info()[2]
    if t is None:
        raise ValueError("A valid traceback must be passed if no "
                         "exception is being handled")

    p = Pdb()
    p.reset()
    p.interaction(None, t)


def pm(Pdb=Pdb):
    post_mortem(sys.last_traceback, Pdb=Pdb)


def cleanup():
    local.GLOBAL_PDB = None
    local._pdbpp_completing = False

# pdb++ specific interface


def xpm(Pdb=Pdb):
    """
    To be used inside an except clause, enter a post-mortem pdb
    related to the just caught exception.
    """
    info = sys.exc_info()
    print(traceback.format_exc())
    post_mortem(info[2], Pdb)


def enable():
    global set_trace
    set_trace = enable.set_trace
    if local.GLOBAL_PDB:
        local.GLOBAL_PDB.disabled = False


enable.set_trace = set_trace


def disable():
    global set_trace
    set_trace = disable.set_trace
    if local.GLOBAL_PDB:
        local.GLOBAL_PDB.disabled = True


disable.set_trace = lambda frame=None, Pdb=Pdb: None


def set_tracex():
    print('PDB!')


set_tracex._dont_inline_ = True

_HIDE_FRAME = object()


def hideframe(func):
    c = func.__code__
    new_co_consts = c.co_consts + (_HIDE_FRAME,)

    if hasattr(c, "replace"):
        # Python 3.8.
        c = c.replace(co_consts=new_co_consts)
    elif sys.version_info < (3, ):
        c = types.CodeType(
            c.co_argcount, c.co_nlocals, c.co_stacksize,
            c.co_flags, c.co_code,
            new_co_consts,
            c.co_names, c.co_varnames, c.co_filename,
            c.co_name, c.co_firstlineno, c.co_lnotab,
            c.co_freevars, c.co_cellvars)
    else:
        # Python 3 takes an additional arg -- kwonlyargcount.
        c = types.CodeType(
            c.co_argcount, c.co_kwonlyargcount, c.co_nlocals, c.co_stacksize,
            c.co_flags, c.co_code,
            c.co_consts + (_HIDE_FRAME,),
            c.co_names, c.co_varnames, c.co_filename,
            c.co_name, c.co_firstlineno, c.co_lnotab,
            c.co_freevars, c.co_cellvars)

    func.__code__ = c
    return func


def always(obj, value):
    return True


def break_on_setattr(attrname, condition=always, Pdb=Pdb):
    def decorator(cls):
        old___setattr__ = cls.__setattr__

        @hideframe
        def __setattr__(self, attr, value):
            if attr == attrname and condition(self, value):
                frame = sys._getframe().f_back
                pdb_ = Pdb()
                pdb_.set_trace(frame)
                pdb_.stopframe = frame
                pdb_.interaction(frame, None)
            old___setattr__(self, attr, value)
        cls.__setattr__ = __setattr__
        return cls
    return decorator


if __name__ == '__main__':
    import pdb
    pdb.main()