Repository: python/cpython
Pull request: gh-102114: Make dis print more concise tracebacks for syntax errors in str inputs
Fixes #102114


<!-- gh-issue-number: gh-102114 -->
* Issue: gh-102114
<!-- /gh-issue-number -->


Problem statement:
dis._try_compile: traceback printed on error in source argument is too wordy
`_try_compile` function from `dis` module firstly tries to compile given source string with `'eval'` mode and, if exception is occured, catches it and tries again with `'exec'` mode. These actions lead to a long chained traceback if given string contains syntax/indentation error, because this string gets refused by `compile` in both `'eval'` and `'exec'` modes.
All functions/constructors from `dis` module that use `_try_compile` and accept source string as an argument (`dis`, `get_instructions`, `code_info`, `show_code`, `Bytecode`) are showing this behavior:
```python
>>> dis.dis(')')
Traceback (most recent call last):
  File "/home/.../cpython/Lib/dis.py", line 67, in _try_compile
    c = compile(source, name, 'eval')
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<dis>", line 1
    )
    ^
SyntaxError: unmatched ')'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/.../cpython/Lib/dis.py", line 112, in dis
    _disassemble_str(x, file=file, depth=depth, show_caches=show_caches, adaptive=adaptive)
  File "/home/.../cpython/Lib/dis.py", line 593, in _disassemble_str
    _disassemble_recursive(_try_compile(source, '<dis>'), **kwargs)
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/.../cpython/Lib/dis.py", line 69, in _try_compile
    c = compile(source, name, 'exec')
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<dis>", line 1
    )
    ^
SyntaxError: unmatched ')'
```
Python versions affected: 3.10.8, 3.11.0, current main.

<!-- gh-linked-prs -->
### Linked PRs
* gh-102115
<!-- /gh-linked-prs -->

Hints from issue discussion:

Commit message: Update 2023-02-21-14-57-34.gh-issue-102114.uUDQzb.rst

Patch to review:
diff --git a/Lib/dis.py b/Lib/dis.py
index b39b2835330135..310ed994d54505 100644
--- a/Lib/dis.py
+++ b/Lib/dis.py
@@ -66,6 +66,8 @@ def _try_compile(source, name):
     try:
         c = compile(source, name, 'eval')
     except SyntaxError:
+        c = None
+    if not c:
         c = compile(source, name, 'exec')
     return c
 
diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py
index 0a60a979614d52..2d5c73c9adc920 100644
--- a/Lib/test/test_dis.py
+++ b/Lib/test/test_dis.py
@@ -1067,6 +1067,13 @@ def check(expected, **kwargs):
         check(dis_nested_2, depth=None)
         check(dis_nested_2)
 
+    def test__try_compile_no_context_exc_on_error(self):
+        # see gh-102114
+        try:
+            dis._try_compile(")", "")
+        except Exception as e:
+            self.assertIsNone(e.__context__)
+
     @staticmethod
     def code_quicken(f, times=ADAPTIVE_WARMUP_DELAY):
         for _ in range(times):
diff --git a/Misc/NEWS.d/next/Library/2023-02-21-14-57-34.gh-issue-102114.uUDQzb.rst b/Misc/NEWS.d/next/Library/2023-02-21-14-57-34.gh-issue-102114.uUDQzb.rst
new file mode 100644
index 00000000000000..4140c9a96cd272
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-02-21-14-57-34.gh-issue-102114.uUDQzb.rst
@@ -0,0 +1 @@
+Functions in the :mod:`dis` module that accept a source code string as argument now print a more concise traceback when the string contains a syntax or indentation error.


Code review comments:
diff_hunk: @@ -66,6 +66,8 @@ def _try_compile(source, name):
     try:
         c = compile(source, name, 'eval')
     except SyntaxError:
+        c = None
+    if not c:
path: Lib/dis.py
text: @user1:
Actually one more issue - it's not obvious to me that compile can never return None (even if it's true now, how do we know it will continue to be true?)

Maybe better to have a boolean variable, set it to ``False`` before the ``try:`` and to ``True`` in the ``except:``, and then check it in the if?

@user1:
Maybe I'm being silly, and it can't be None because it's supposed to be a code object or an ast. In that case maybe just change the check to "if c is None", instead of relying on the bool() value of the result.

@author:
For me it looks like call to `builtin_compile` always utilizes code object created by `_PyAssemble_MakeCodeObject` or AST returned by `PyAST_mod2obj` call, or sets exception and returns `NULL` if error is occurred, so its result can't be `None`.

diff_hunk: @@ -66,6 +66,8 @@ def _try_compile(source, name):
     try:
         c = compile(source, name, 'eval')
     except SyntaxError:
+        c = None
+    if not c:
path: Lib/dis.py
text: @user1:
```suggestion
    if c is None:
```