IronLanguages / IronLanguages/ironpython3

Stack info lost when handling exception from thread

Open
#2,073 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
2.8k
Forks
316
Avg merge
1d 9h
Merged PRs (30d)
1

Description

IronPython appears to lose frames from within the thread, where they are retained in CPython

(May be duplicate of #738 , or perhaps just related, but it seemed worth submitting the tests.)

Minimal repro.

import functools
import sys
import threading
import traceback
import unittest

print(sys.version)

class AsyncTask(object):
    def __init__(self, func):
        functools.wraps(func)(self)
        self._func = func
        self._result = None
        self._exception = None

    def __call__(self, *args, **kwargs):
        self._thread = threading.Thread(target=self._run, args=args, kwargs=kwargs)
        self._thread.start()
        return self

    def _run(self, *args, **kwargs):
        try:
            self._result = self._func(*args, **kwargs)
        except Exception as err:
            self._exception = err
        finally:
            self._complete = True

    @property
    def result(self):
        if self._thread is None:
            raise RuntimeError("Not started.")
        self._thread.join()
        if self._exception is not None:
            raise self._exception
        return self._result

    def await_task(self, timeout=None):
        if self._thread is None:
            raise RuntimeError("Not started.")
        self._thread.join(timeout)
        completed = not self._thread.is_alive()
        if completed and self._exception is not None:
            raise self._exception
        return completed


def module_level_raising_function(exc):
    raise exc


class TestAsyncTaskException(unittest.TestCase):
    def test_with_function_scoped_def(self):
        def raising_function(exc):
            raise exc

        return self._test(raising_function)

    def test_with_module_scoped_def(self):
        self._test(module_level_raising_function)

    def _test(self, raising_func):
        the_exception = Exception('the exception message')
        task = AsyncTask(raising_func)(the_exception)
        caught_exception = None
        try:
            task.result
        except Exception as caught_exception:
            etype, evalue, etb = sys.exc_info()
            trace = traceback.extract_tb(etb)
            self.assertIs(caught_exception, the_exception)

            print("\nTraceback for {}:\n{}".format(self._testMethodName, '\n'.join(traceback.format_tb(etb))))

            (filenames, line_numbers, function_names, texts) = zip(*trace)
            self.assertIn(raising_func.__name__, function_names)
        else:
            self.fail("Exception not raised.")


if __name__ == '__main__':
    unittest.main()

Expected result - CPython 3.4

3.4.0 (v3.4.0:04f714765c13, Mar 16 2014, 19:24:06) [MSC v.1600 32 bit (Intel)]

Traceback for test_with_function_scoped_def:
  File "C:\Users\michael.phillips\repos\AsyncTask\minrepro.py", line 68, in _test
    task.result

  File "C:\Users\michael.phillips\repos\AsyncTask\minrepro.py", line 36, in result
    raise self._exception

  File "C:\Users\michael.phillips\repos\AsyncTask\minrepro.py", line 24, in _run
    self._result = self._func(*args, **kwargs)

  File "C:\Users\michael.phillips\repos\AsyncTask\minrepro.py", line 56, in raising_function
    raise exc

.
Traceback for test_with_module_scoped_def:
  File "C:\Users\michael.phillips\repos\AsyncTask\minrepro.py", line 68, in _test
    task.result

  File "C:\Users\michael.phillips\repos\AsyncTask\minrepro.py", line 36, in result
    raise self._exception

  File "C:\Users\michael.phillips\repos\AsyncTask\minrepro.py", line 24, in _run
    self._result = self._func(*args, **kwargs)

  File "C:\Users\michael.phillips\repos\AsyncTask\minrepro.py", line 50, in module_level_raising_function
    raise exc

.
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Observed result - IronPython 3.4

[.NETFramework,Version=v4.6.2 on .NET Framework 4.8.9324.0 (64-bit)]

Traceback for test_with_function_scoped_def:
  File ".\minrepro.py", line 68, in _test
    task.result

  File ".\minrepro.py", line 36, in result
    raise self._exception

F
Traceback for test_with_module_scoped_def:
  File ".\minrepro.py", line 68, in _test
    task.result

  File ".\minrepro.py", line 36, in result
    raise self._exception

F
======================================================================
FAIL: test_with_function_scoped_def (minrepro.TestAsyncTaskException)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ".\minrepro.py", line 58, in test_with_function_scoped_def
    return self._test(raising_function)
  File ".\minrepro.py", line 77, in _test
    self.assertIn(raising_func.__name__, function_names)
AssertionError: 'raising_function' not found in ('_test', 'result')

======================================================================
FAIL: test_with_module_scoped_def (minrepro.TestAsyncTaskException)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ".\minrepro.py", line 61, in test_with_module_scoped_def
    self._test(module_level_raising_function)
  File ".\minrepro.py", line 77, in _test
    self.assertIn(raising_func.__name__, function_names)
AssertionError: 'module_level_raising_function' not found in ('_test', 'result')

----------------------------------------------------------------------
Ran 2 tests in 0.620s

FAILED (failures=2)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the minimal repro in minrepro.py under IronPython and CPython, focusing on AsyncTask._run and the result property where the exception crosses the thread boundary. Done means the IronPython traceback retains the raising_function or module_level_raising_function frame, matching the expected CPython output and passing both unittest cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.