Repository: gotcha/ipdb

Issue:
Importing ipdb initializes IPython
Currently, merely importing `ipdb` causes it to initialize IPython. In turn, among other things, IPython installs its excepthook. Therefore on programs that import `ipdb`, the default exception behaviour is already wrapped with IPython's, even when both ipdb and IPython aren't run.

For example, take this program:

```
$ cat test.py
import ipdb

1 / 0
```

Running it with the latest `master` of `ipdb`, and most released versions, gives this error message:

```
$ python test.py
Traceback (most recent call last):
  File "/.../ipdb/test.py", line 3, in <module>
    1 / 0
ZeroDivisionError: division by zero

If you suspect this is an IPython 7.21.0 bug, please report it at:
    https://github.com/ipython/ipython/issues
or send an email to the mailing list at ipython-dev@python.org

You can print a more detailed traceback right now with "%tb", or use "%debug"
to interactively debug it.

Extra-detailed tracebacks for bug-reporting purposes can be enabled via:
    %config Application.verbose_crash=True
```

The IPython banner doesn't make any sense since, whilst instantiated, IPython was not used. I'd expect only Python's default exception output:

```
python test.py
Traceback (most recent call last):
  File "/.../ipdb/test.py", line 3, in <module>
    1 / 0
ZeroDivisionError: division by zero
```

This probably affects a lot of users through at least Django's test runner, which imports `ipdb` if available.

Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/gotcha/ipdb/pull/224?src=pr&el=h1) Report
> Merging [#224](https://codecov.io/gh/gotcha/ipdb/pull/224?src=pr&el=desc) (94428c2) into [master](https://codecov.io/gh/gotcha/ipdb/commit/5bb1e869d7b01ca6f5891d297ae66907614ee67a?el=desc) (5bb1e86) will **decrease** coverage by `1.46%`.
> The diff coverage is `47.05%`.

[![Impacted file tree graph](https://codecov.io/gh/gotcha/ipdb/pull/224/graphs/tree.svg?width=650&height=150&src=pr&token=rLlY1IZ1E0)](https://codecov.io/gh/gotcha/ipdb/pull/224?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #224      +/-   ##
==========================================
- Coverage   81.15%   79.69%   -1.47%     
==========================================
  Files           7        7              
  Lines         451      453       +2     
==========================================
- Hits          366      361       -5     
- Misses         85       92       +7     
```


| [Impacted Files](https://codecov.io/gh/gotcha/ipdb/pull/224?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [ipdb/\_\_main\_\_.py](https://codecov.io/gh/gotcha/ipdb/pull/224/diff?src=pr&el=tree#diff-aXBkYi9fX21haW5fXy5weQ==) | `64.94% <18.18%> (-3.29%)` | :arrow_down: |
| [tests/test\_opts.py](https://codecov.io/gh/gotcha/ipdb/pull/224/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9vcHRzLnB5) | `94.73% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/gotcha/ipdb/pull/224?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/gotcha/ipdb/pull/224?src=pr&el=footer). Last update [5bb1e86...94428c2](https://codecov.io/gh/gotcha/ipdb/pull/224?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/ipdb/__main__.py b/ipdb/__main__.py
index 8792b73..52e26e3 100644
--- a/ipdb/__main__.py
+++ b/ipdb/__main__.py
@@ -22,34 +22,36 @@ except:
     import ConfigParser as configparser
 
 
-shell = get_ipython()
-if shell is None:
-    # Not inside IPython
-    # Build a terminal app in order to force ipython to load the
-    # configuration
-    ipapp = TerminalIPythonApp()
-    # Avoid output (banner, prints)
-    ipapp.interact = False
-    ipapp.initialize(['--no-term-title'])
-    shell = ipapp.shell
-else:
-    # Running inside IPython
-
-    # Detect if embed shell or not and display a message
-    if isinstance(shell, InteractiveShellEmbed):
-        sys.stderr.write(
-            "\nYou are currently into an embedded ipython shell,\n"
-            "the configuration will not be loaded.\n\n"
-        )
+def _get_debugger_cls():
+    shell = get_ipython()
+    if shell is None:
+        # Not inside IPython
+        # Build a terminal app in order to force ipython to load the
+        # configuration
+        ipapp = TerminalIPythonApp()
+        # Avoid output (banner, prints)
+        ipapp.interact = False
+        ipapp.initialize(["--no-term-title"])
+        shell = ipapp.shell
+    else:
+        # Running inside IPython
+
+        # Detect if embed shell or not and display a message
+        if isinstance(shell, InteractiveShellEmbed):
+            sys.stderr.write(
+                "\nYou are currently into an embedded ipython shell,\n"
+                "the configuration will not be loaded.\n\n"
+            )
 
-# Let IPython decide about which debugger class to use
-# This is especially important for tools that fiddle with stdout
-debugger_cls = shell.debugger_cls
+    # Let IPython decide about which debugger class to use
+    # This is especially important for tools that fiddle with stdout
+    return shell.debugger_cls
 
 
 def _init_pdb(context=None, commands=[]):
     if context is None:
         context = os.getenv("IPDB_CONTEXT_SIZE", get_context_from_config())
+    debugger_cls = _get_debugger_cls()
     try:
         p = debugger_cls(context=context)
     except TypeError:


Test patch:
diff --git a/tests/test_opts.py b/tests/test_opts.py
index f4ed26d..066f64f 100644
--- a/tests/test_opts.py
+++ b/tests/test_opts.py
@@ -14,7 +14,7 @@ from getopt import GetoptError
 from ipdb.__main__ import main
 
 
-@patch('ipdb.__main__.debugger_cls')
+@patch('ipdb.__main__._get_debugger_cls')
 class OptsTest(unittest.TestCase):
     def set_argv(self, *argv):
         argv_patch = patch('ipdb.__main__.sys.argv', argv)
@@ -22,26 +22,26 @@ class OptsTest(unittest.TestCase):
         self.addCleanup(argv_patch.stop)
 
     @patch('ipdb.__main__.sys.version_info', (3, 7))
-    def test_debug_module_script(self, debugger_cls):
+    def test_debug_module_script(self, get_debugger_cls):
         module_name = 'my_buggy_module'
         self.set_argv('ipdb', '-m', module_name)
 
         main()
 
-        debugger = debugger_cls.return_value
+        debugger = get_debugger_cls.return_value.return_value
         debugger._runmodule.assert_called_once_with(module_name)
 
     @patch('ipdb.__main__.os.path.exists')
-    def test_debug_script(self, exists, debugger_cls):
+    def test_debug_script(self, exists, get_debugger_cls):
         script_name = 'my_buggy_script'
         self.set_argv('ipdb', script_name)
 
         main()
 
-        debugger = debugger_cls.return_value
+        debugger = get_debugger_cls.return_value.return_value
         debugger._runscript.assert_called_once_with(script_name)
 
-    def test_option_m_fallback_on_py36(self, debugger_cls):
+    def test_option_m_fallback_on_py36(self, get_debugger_cls):
         self.set_argv('ipdb', '-m', 'my.module')
         with patch('ipdb.__main__.sys.version_info', (3, 6)):
             with self.assertRaises(GetoptError):