From 3c05dfadbb1b36ce4b55b614c158a6539314ee52 Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Tue, 24 Mar 2026 17:54:33 +0100 Subject: [PATCH 1/8] [INS-985] Pop on unreadable files --- backtracepython/source_code_handler.py | 2 +- tests/test_source_code_handler.py | 67 ++++++++++++++++++++++++++ tox.ini | 2 +- 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 tests/test_source_code_handler.py diff --git a/backtracepython/source_code_handler.py b/backtracepython/source_code_handler.py index e88d36e..53af0e5 100644 --- a/backtracepython/source_code_handler.py +++ b/backtracepython/source_code_handler.py @@ -42,7 +42,7 @@ def collect(self, report): if new_max_line > source["maxLine"]: source["maxLine"] = new_max_line - for source_code_path in source_code: + for source_code_path in list(source_code): source = source_code[source_code_path] source_code_content = self.read_source( source_code_path, source["startLine"] - 1, source["maxLine"] diff --git a/tests/test_source_code_handler.py b/tests/test_source_code_handler.py new file mode 100644 index 0000000..2f9284e --- /dev/null +++ b/tests/test_source_code_handler.py @@ -0,0 +1,67 @@ +from backtracepython.source_code_handler import SourceCodeHandler + + +def make_report(source_paths): + """Build a minimal report whose main thread stack references the given file paths.""" + stack = [ + {"sourceCode": path, "line": 10, "funcName": "test"} + for path in source_paths + ] + return { + "mainThread": "main", + "threads": { + "main": {"stack": stack}, + }, + } + + +def test_collect_removes_unreadable_sources_without_runtime_error(): + """Reproduces RuntimeError: dictionary changed size during iteration. + + When every source file in the stack is unreadable, collect() used to pop + entries from the source_code dict while iterating over it. + """ + handler = SourceCodeHandler(tab_width=4, context_line_count=3) + report = make_report([ + "/nonexistent/path/a.py", + "/nonexistent/path/b.py", + ]) + + # Before the fix this raised: + # RuntimeError: dictionary changed size during iteration + result = handler.collect(report) + + assert result["sourceCode"] == {} + + +def test_collect_keeps_readable_sources(tmp_path): + """Verify that readable source files are collected normally.""" + source_file = tmp_path / "real.py" + source_file.write_text("line1\nline2\nline3\nline4\nline5\n") + + handler = SourceCodeHandler(tab_width=4, context_line_count=1) + report = make_report([str(source_file)]) + + result = handler.collect(report) + + assert str(source_file) in result["sourceCode"] + assert "text" in result["sourceCode"][str(source_file)] + + +def test_collect_mixed_readable_and_unreadable(tmp_path): + """Mix of existing and missing files — only readable ones should survive.""" + source_file = tmp_path / "exists.py" + source_file.write_text("a = 1\nb = 2\n" * 10) + + handler = SourceCodeHandler(tab_width=4, context_line_count=3) + report = make_report([ + "/nonexistent/path/missing.py", + str(source_file), + "/another/missing/file.py", + ]) + + result = handler.collect(report) + + assert str(source_file) in result["sourceCode"] + assert "/nonexistent/path/missing.py" not in result["sourceCode"] + assert "/another/missing/file.py" not in result["sourceCode"] diff --git a/tox.ini b/tox.ini index 4dd7c9f..cb945c5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, py37, py38, py39, py310 +envlist = py27, py37, py38, py39, py310, py312, py313 skipsdist = True [testenv] From 41ce30486b5160c315a07cddc29d60f3b7b5e289 Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Tue, 24 Mar 2026 17:57:39 +0100 Subject: [PATCH 2/8] Fix formatting --- tests/test_source_code_handler.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/tests/test_source_code_handler.py b/tests/test_source_code_handler.py index 2f9284e..849425d 100644 --- a/tests/test_source_code_handler.py +++ b/tests/test_source_code_handler.py @@ -4,8 +4,7 @@ def make_report(source_paths): """Build a minimal report whose main thread stack references the given file paths.""" stack = [ - {"sourceCode": path, "line": 10, "funcName": "test"} - for path in source_paths + {"sourceCode": path, "line": 10, "funcName": "test"} for path in source_paths ] return { "mainThread": "main", @@ -22,10 +21,12 @@ def test_collect_removes_unreadable_sources_without_runtime_error(): entries from the source_code dict while iterating over it. """ handler = SourceCodeHandler(tab_width=4, context_line_count=3) - report = make_report([ - "/nonexistent/path/a.py", - "/nonexistent/path/b.py", - ]) + report = make_report( + [ + "/nonexistent/path/a.py", + "/nonexistent/path/b.py", + ] + ) # Before the fix this raised: # RuntimeError: dictionary changed size during iteration @@ -54,11 +55,13 @@ def test_collect_mixed_readable_and_unreadable(tmp_path): source_file.write_text("a = 1\nb = 2\n" * 10) handler = SourceCodeHandler(tab_width=4, context_line_count=3) - report = make_report([ - "/nonexistent/path/missing.py", - str(source_file), - "/another/missing/file.py", - ]) + report = make_report( + [ + "/nonexistent/path/missing.py", + str(source_file), + "/another/missing/file.py", + ] + ) result = handler.collect(report) From 82b8183ffd7d731a647cfa8271a0365d1e8e600e Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Tue, 24 Mar 2026 17:59:13 +0100 Subject: [PATCH 3/8] Simplify input --- tests/test_source_code_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_source_code_handler.py b/tests/test_source_code_handler.py index 849425d..8620c49 100644 --- a/tests/test_source_code_handler.py +++ b/tests/test_source_code_handler.py @@ -38,7 +38,7 @@ def test_collect_removes_unreadable_sources_without_runtime_error(): def test_collect_keeps_readable_sources(tmp_path): """Verify that readable source files are collected normally.""" source_file = tmp_path / "real.py" - source_file.write_text("line1\nline2\nline3\nline4\nline5\n") + source_file.write_text("foobarbaz") handler = SourceCodeHandler(tab_width=4, context_line_count=1) report = make_report([str(source_file)]) @@ -52,7 +52,7 @@ def test_collect_keeps_readable_sources(tmp_path): def test_collect_mixed_readable_and_unreadable(tmp_path): """Mix of existing and missing files — only readable ones should survive.""" source_file = tmp_path / "exists.py" - source_file.write_text("a = 1\nb = 2\n" * 10) + source_file.write_text("foobarbaz") handler = SourceCodeHandler(tab_width=4, context_line_count=3) report = make_report( From ce9344f31593150a4fd2058f0b3cfd6aaacf9a17 Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Tue, 24 Mar 2026 18:18:13 +0100 Subject: [PATCH 4/8] Remove invalid char for python2 --- tests/test_source_code_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_source_code_handler.py b/tests/test_source_code_handler.py index 8620c49..83e0199 100644 --- a/tests/test_source_code_handler.py +++ b/tests/test_source_code_handler.py @@ -50,7 +50,7 @@ def test_collect_keeps_readable_sources(tmp_path): def test_collect_mixed_readable_and_unreadable(tmp_path): - """Mix of existing and missing files — only readable ones should survive.""" + """Mix of existing and missing files""" source_file = tmp_path / "exists.py" source_file.write_text("foobarbaz") From 5c6d8e72c296784986aa24608d74937ac889735a Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Tue, 24 Mar 2026 18:19:46 +0100 Subject: [PATCH 5/8] Test the most important python versions --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c9fbdc..a131e6f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python-version: [3.7, 3.8, 3.9, 3.11] # List of Python versions to test against + python-version: [3.7, 3.8, 3.9, 3.11, 3.12, 3.13] # List of Python versions to test against steps: - name: Checkout code @@ -46,7 +46,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.11 + python-version: 3.13 update-environment: false - name: Install dependencies From 02432ac3e8773cc6adbb8ea9d24f18f573a86261 Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Tue, 24 Mar 2026 18:30:33 +0100 Subject: [PATCH 6/8] Fix writing files to temp dir --- tests/test_source_code_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_source_code_handler.py b/tests/test_source_code_handler.py index 83e0199..8d0d1fd 100644 --- a/tests/test_source_code_handler.py +++ b/tests/test_source_code_handler.py @@ -38,7 +38,7 @@ def test_collect_removes_unreadable_sources_without_runtime_error(): def test_collect_keeps_readable_sources(tmp_path): """Verify that readable source files are collected normally.""" source_file = tmp_path / "real.py" - source_file.write_text("foobarbaz") + source_file.write_text(u"foobarbaz") handler = SourceCodeHandler(tab_width=4, context_line_count=1) report = make_report([str(source_file)]) @@ -52,7 +52,7 @@ def test_collect_keeps_readable_sources(tmp_path): def test_collect_mixed_readable_and_unreadable(tmp_path): """Mix of existing and missing files""" source_file = tmp_path / "exists.py" - source_file.write_text("foobarbaz") + source_file.write_text(u"foobarbaz") handler = SourceCodeHandler(tab_width=4, context_line_count=3) report = make_report( From 3fe6c24f6263dc97ba365b808688302e953fa673 Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Tue, 24 Mar 2026 20:30:49 +0100 Subject: [PATCH 7/8] properly solve compatibility issue py2 vs py3 --- tests/test_source_code_handler.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_source_code_handler.py b/tests/test_source_code_handler.py index 8d0d1fd..562998e 100644 --- a/tests/test_source_code_handler.py +++ b/tests/test_source_code_handler.py @@ -1,6 +1,13 @@ +import os + from backtracepython.source_code_handler import SourceCodeHandler +def write_file(path, content): + with open(str(path), "w") as f: + f.write(content) + + def make_report(source_paths): """Build a minimal report whose main thread stack references the given file paths.""" stack = [ @@ -38,7 +45,7 @@ def test_collect_removes_unreadable_sources_without_runtime_error(): def test_collect_keeps_readable_sources(tmp_path): """Verify that readable source files are collected normally.""" source_file = tmp_path / "real.py" - source_file.write_text(u"foobarbaz") + write_file(source_file, "foobarbaz") handler = SourceCodeHandler(tab_width=4, context_line_count=1) report = make_report([str(source_file)]) @@ -52,7 +59,7 @@ def test_collect_keeps_readable_sources(tmp_path): def test_collect_mixed_readable_and_unreadable(tmp_path): """Mix of existing and missing files""" source_file = tmp_path / "exists.py" - source_file.write_text(u"foobarbaz") + write_file(source_file, "foobarbaz") handler = SourceCodeHandler(tab_width=4, context_line_count=3) report = make_report( From 9e64905903557d620e1d24e93e45cde306a34bd2 Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Tue, 24 Mar 2026 20:37:57 +0100 Subject: [PATCH 8/8] Version bump --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e692a8..f826279 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# Version 0.4.3 + +- Fixed invalid source code behavior when the source file doesn't exist or couldn't be read (#28) + # Version 0.4.2 - Fixed a bug that can mark 2+ threads as a faulting thread (#26), diff --git a/setup.py b/setup.py index c77e1be..a7bcab8 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="backtracepython", - version="0.4.2", + version="0.4.3", description="Backtrace.io error reporting tool for Python", author="Backtrace.io", author_email="team@backtrace.io",