1# Copyright (c) 2011-2016 Godefroid Chapelle and ipdb development team
2#
3# This file is part of ipdb.
4# Redistributable under the revised BSD license
5# https://opensource.org/licenses/BSD-3-Clause
6
7from __future__ import print_function
8import os
9import sys
10
11from decorator import contextmanager
12
13__version__ = '0.13.9'
14
15from IPython import get_ipython
16from IPython.core.debugger import BdbQuit_excepthook
17from IPython.terminal.ipapp import TerminalIPythonApp
18from IPython.terminal.embed import InteractiveShellEmbed
19try:
20    import configparser
21except:
22    import ConfigParser as configparser
23
24
25def _get_debugger_cls():
26    shell = get_ipython()
27    if shell is None:
28        # Not inside IPython
29        # Build a terminal app in order to force ipython to load the
30        # configuration
31        ipapp = TerminalIPythonApp()
32        # Avoid output (banner, prints)
33        ipapp.interact = False
34        ipapp.initialize(["--no-term-title"])
35        shell = ipapp.shell
36    else:
37        # Running inside IPython
38
39        # Detect if embed shell or not and display a message
40        if isinstance(shell, InteractiveShellEmbed):
41            sys.stderr.write(
42                "\nYou are currently into an embedded ipython shell,\n"
43                "the configuration will not be loaded.\n\n"
44            )
45
46    # Let IPython decide about which debugger class to use
47    # This is especially important for tools that fiddle with stdout
48    return shell.debugger_cls
49
50
51def _init_pdb(context=None, commands=[]):
52    if context is None:
53        context = os.getenv("IPDB_CONTEXT_SIZE", get_context_from_config())
54    debugger_cls = _get_debugger_cls()
55    try:
56        p = debugger_cls(context=context)
57    except TypeError:
58        p = debugger_cls()
59    p.rcLines.extend(commands)
60    return p
61
62
63def wrap_sys_excepthook():
64    # make sure we wrap it only once or we would end up with a cycle
65    #  BdbQuit_excepthook.excepthook_ori == BdbQuit_excepthook
66    if sys.excepthook != BdbQuit_excepthook:
67        BdbQuit_excepthook.excepthook_ori = sys.excepthook
68        sys.excepthook = BdbQuit_excepthook
69
70
71def set_trace(frame=None, context=None, cond=True):
72    if not cond:
73        return
74    wrap_sys_excepthook()
75    if frame is None:
76        frame = sys._getframe().f_back
77    p = _init_pdb(context).set_trace(frame)
78    if p and hasattr(p, 'shell'):
79        p.shell.restore_sys_module_state()
80
81
82def get_context_from_config():
83    try:
84        parser = get_config()
85        return parser.getint("ipdb", "context")
86    except (configparser.NoSectionError, configparser.NoOptionError):
87        return 3
88    except ValueError:
89        value = parser.get("ipdb", "context")
90        raise ValueError(
91            "In %s,  context value [%s] cannot be converted into an integer."
92            % (parser.filepath, value)
93        )
94
95
96class ConfigFile(object):
97    """
98    Filehandle wrapper that adds a "[ipdb]" section to the start of a config
99    file so that users don't actually have to manually add a [ipdb] section.
100    Works with configparser versions from both Python 2 and 3
101    """
102
103    def __init__(self, filepath):
104        self.first = True
105        with open(filepath) as f:
106            self.lines = f.readlines()
107
108    # Python 2.7 (Older dot versions)
109    def readline(self):
110        try:
111            return self.__next__()
112        except StopIteration:
113            return ''
114
115    # Python 2.7 (Newer dot versions)
116    def next(self):
117        return self.__next__()
118
119    # Python 3
120    def __iter__(self):
121        return self
122
123    def __next__(self):
124        if self.first:
125            self.first = False
126            return "[ipdb]\n"
127        if self.lines:
128            return self.lines.pop(0)
129        raise StopIteration
130
131
132def get_config():
133    """
134    Get ipdb config file settings.
135    All available config files are read.  If settings are in multiple configs,
136    the last value encountered wins.  Values specified on the command-line take
137    precedence over all config file settings.
138    Returns: A ConfigParser object.
139    """
140    parser = configparser.ConfigParser()
141
142    filepaths = []
143
144    # Low priority goes first in the list
145    for cfg_file in ("setup.cfg", ".ipdb", "pyproject.toml"):
146        cwd_filepath = os.path.join(os.getcwd(), cfg_file)
147        if os.path.isfile(cwd_filepath):
148            filepaths.append(cwd_filepath)
149
150    # Medium priority (whenever user wants to set a specific path to config file)
151    home = os.getenv("HOME")
152    if home:
153        default_filepath = os.path.join(home, ".ipdb")
154        if os.path.isfile(default_filepath):
155            filepaths.append(default_filepath)
156
157    # High priority (default files)
158    env_filepath = os.getenv("IPDB_CONFIG")
159    if env_filepath and os.path.isfile(env_filepath):
160        filepaths.append(env_filepath)
161
162    if filepaths:
163        # Python 3 has parser.read_file(iterator) while Python2 has
164        # parser.readfp(obj_with_readline)
165        try:
166            read_func = parser.read_file
167        except AttributeError:
168            read_func = parser.readfp
169        for filepath in filepaths:
170            parser.filepath = filepath
171            # Users are expected to put an [ipdb] section
172            # only if they use setup.cfg
173            if filepath.endswith('setup.cfg'):
174                with open(filepath) as f:
175                    parser.remove_section("ipdb")
176                    read_func(f)
177            # To use on pyproject.toml, put [tool.ipdb] section
178            elif filepath.endswith('pyproject.toml'):
179                import toml
180                toml_file = toml.load(filepath)
181                if "tool" in toml_file and "ipdb" in toml_file["tool"]:
182                    if not parser.has_section("ipdb"):
183                        parser.add_section("ipdb")
184                    for key, value in toml_file["tool"]["ipdb"].items():
185                        parser.set("ipdb", key, str(value))
186            else:
187                read_func(ConfigFile(filepath))
188    return parser
189
190
191def post_mortem(tb=None):
192    wrap_sys_excepthook()
193    p = _init_pdb()
194    p.reset()
195    if tb is None:
196        # sys.exc_info() returns (type, value, traceback) if an exception is
197        # being handled, otherwise it returns None
198        tb = sys.exc_info()[2]
199    if tb:
200        p.interaction(None, tb)
201
202
203def pm():
204    post_mortem(sys.last_traceback)
205
206
207def run(statement, globals=None, locals=None):
208    _init_pdb().run(statement, globals, locals)
209
210
211def runcall(*args, **kwargs):
212    return _init_pdb().runcall(*args, **kwargs)
213
214
215def runeval(expression, globals=None, locals=None):
216    return _init_pdb().runeval(expression, globals, locals)
217
218
219@contextmanager
220def launch_ipdb_on_exception():
221    try:
222        yield
223    except Exception:
224        e, m, tb = sys.exc_info()
225        print(m.__repr__(), file=sys.stderr)
226        post_mortem(tb)
227    finally:
228        pass
229
230
231# iex is a concise alias
232iex = launch_ipdb_on_exception()
233
234
235_usage = """\
236usage: python -m ipdb [-m] [-c command] ... pyfile [arg] ...
237
238Debug the Python program given by pyfile.
239
240Initial commands are read from .pdbrc files in your home directory
241and in the current directory, if they exist.  Commands supplied with
242-c are executed after commands from .pdbrc files.
243
244To let the script run until an exception occurs, use "-c continue".
245To let the script run up to a given line X in the debugged file, use
246"-c 'until X'"
247
248Option -m is available only in Python 3.7 and later.
249
250ipdb version %s.""" % __version__
251
252
253def main():
254    import traceback
255    import sys
256    import getopt
257
258    try:
259        from pdb import Restart
260    except ImportError:
261        class Restart(Exception):
262            pass
263
264    if sys.version_info >= (3, 7):
265        opts, args = getopt.getopt(sys.argv[1:], 'mhc:', ['help', 'command='])
266    else:
267        opts, args = getopt.getopt(sys.argv[1:], 'hc:', ['help', 'command='])
268
269    commands = []
270    run_as_module = False
271    for opt, optarg in opts:
272        if opt in ['-h', '--help']:
273            print(_usage)
274            sys.exit()
275        elif opt in ['-c', '--command']:
276            commands.append(optarg)
277        elif opt in ['-m']:
278            run_as_module = True
279
280    if not args:
281        print(_usage)
282        sys.exit(2)
283
284    mainpyfile = args[0]     # Get script filename
285    if not run_as_module and not os.path.exists(mainpyfile):
286        print('Error:', mainpyfile, 'does not exist')
287        sys.exit(1)
288
289    sys.argv = args     # Hide "pdb.py" from argument list
290
291    # Replace pdb's dir with script's dir in front of module search path.
292    if not run_as_module:
293        sys.path[0] = os.path.dirname(mainpyfile)
294
295    # Note on saving/restoring sys.argv: it's a good idea when sys.argv was
296    # modified by the script being debugged. It's a bad idea when it was
297    # changed by the user from the command line. There is a "restart" command
298    # which allows explicit specification of command line arguments.
299    pdb = _init_pdb(commands=commands)
300    while 1:
301        try:
302            if run_as_module:
303                pdb._runmodule(mainpyfile)
304            else:
305                pdb._runscript(mainpyfile)
306            if pdb._user_requested_quit:
307                break
308            print("The program finished and will be restarted")
309        except Restart:
310            print("Restarting", mainpyfile, "with arguments:")
311            print("\t" + " ".join(sys.argv[1:]))
312        except SystemExit:
313            # In most cases SystemExit does not warrant a post-mortem session.
314            print("The program exited via sys.exit(). Exit status: ", end='')
315            print(sys.exc_info()[1])
316        except:
317            traceback.print_exc()
318            print("Uncaught exception. Entering post mortem debugging")
319            print("Running 'cont' or 'step' will restart the program")
320            t = sys.exc_info()[2]
321            pdb.interaction(None, t)
322            print("Post mortem debugger finished. The " + mainpyfile +
323                  " will be restarted")
324
325
326if __name__ == '__main__':
327    main()
328