From 50b09112b828d183838a4f027d0224c568e87f54 Mon Sep 17 00:00:00 2001 From: Ghraven Date: Sun, 30 Aug 2026 19:18:19 +0800 Subject: [PATCH 1/3] fix(streamlit): read state logs as utf-8 --- burr/integrations/streamlit.py | 2 +- tests/integrations/test_streamlit.py | 65 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 tests/integrations/test_streamlit.py diff --git a/burr/integrations/streamlit.py b/burr/integrations/streamlit.py index 71b63bce0..c3b1df159 100644 --- a/burr/integrations/streamlit.py +++ b/burr/integrations/streamlit.py @@ -95,7 +95,7 @@ def load_state_from_log_file(jsonl_log_file: str, app: Application) -> AppState: :return: AppState """ out = [] - for i, line in enumerate(open(jsonl_log_file)): + for i, line in enumerate(open(jsonl_log_file, encoding="utf-8")): json_line = json.loads(line) record = Record( state=json_line["state"], diff --git a/tests/integrations/test_streamlit.py b/tests/integrations/test_streamlit.py new file mode 100644 index 000000000..122b0e689 --- /dev/null +++ b/tests/integrations/test_streamlit.py @@ -0,0 +1,65 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import builtins +import importlib +import json +import sys +import types + + +def test_load_state_from_log_file_reads_utf8(monkeypatch, tmp_path): + monkeypatch.setitem( + sys.modules, + "burr.integrations.hamilton", + types.SimpleNamespace(Hamilton=object, StateSource=object), + ) + monkeypatch.setitem(sys.modules, "graphviz", types.SimpleNamespace(Digraph=object)) + monkeypatch.setitem( + sys.modules, "streamlit", types.SimpleNamespace(session_state={}) + ) + streamlit = importlib.import_module("burr.integrations.streamlit") + + log_file = tmp_path / "state.jsonl" + log_file.write_text( + json.dumps( + { + "state": {"message": "café"}, + "action": "say", + "result": {"ok": True}, + }, + ensure_ascii=False, + ) + + "\n", + encoding="utf-8", + ) + + real_open = builtins.open + + def guarded_open(*args, **kwargs): + assert kwargs.get("encoding") == "utf-8" + return real_open(*args, **kwargs) + + monkeypatch.setattr(builtins, "open", guarded_open) + + app = object() + state = streamlit.load_state_from_log_file(str(log_file), app) + + assert state.app is app + assert state.history[0].state == {"message": "café"} + assert state.history[0].action == "say" + assert state.history[0].result == {"ok": True} From c31b0c2129553074fd70f086b576218507a2a4eb Mon Sep 17 00:00:00 2001 From: Ghraven Date: Mon, 5 Oct 2026 09:25:02 +0800 Subject: [PATCH 2/3] test: isolate streamlit optional dependencies and close log files --- burr/integrations/streamlit.py | 19 ++++++++++--------- tests/integrations/test_streamlit.py | 23 +++++++++++++++++++---- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/burr/integrations/streamlit.py b/burr/integrations/streamlit.py index c3b1df159..bb9fd9e9e 100644 --- a/burr/integrations/streamlit.py +++ b/burr/integrations/streamlit.py @@ -95,15 +95,16 @@ def load_state_from_log_file(jsonl_log_file: str, app: Application) -> AppState: :return: AppState """ out = [] - for i, line in enumerate(open(jsonl_log_file, encoding="utf-8")): - json_line = json.loads(line) - record = Record( - state=json_line["state"], - action=json_line["action"], - result=json_line["result"] - # TODO -- add start time, end time - ) - out.append(record) + with open(jsonl_log_file, encoding="utf-8") as log_file: + for line in log_file: + json_line = json.loads(line) + record = Record( + state=json_line["state"], + action=json_line["action"], + result=json_line["result"] + # TODO -- add start time, end time + ) + out.append(record) return AppState(display_index=len(out) - 1, history=out, app=app) diff --git a/tests/integrations/test_streamlit.py b/tests/integrations/test_streamlit.py index 122b0e689..149084aab 100644 --- a/tests/integrations/test_streamlit.py +++ b/tests/integrations/test_streamlit.py @@ -20,6 +20,7 @@ import json import sys import types +from pathlib import Path def test_load_state_from_log_file_reads_utf8(monkeypatch, tmp_path): @@ -29,10 +30,20 @@ def test_load_state_from_log_file_reads_utf8(monkeypatch, tmp_path): types.SimpleNamespace(Hamilton=object, StateSource=object), ) monkeypatch.setitem(sys.modules, "graphviz", types.SimpleNamespace(Digraph=object)) - monkeypatch.setitem( - sys.modules, "streamlit", types.SimpleNamespace(session_state={}) + monkeypatch.setitem(sys.modules, "streamlit", types.SimpleNamespace(session_state={})) + colors = types.ModuleType("matplotlib.colors") + matplotlib = types.ModuleType("matplotlib") + matplotlib.colors = colors + monkeypatch.setitem(sys.modules, "matplotlib", matplotlib) + monkeypatch.setitem(sys.modules, "matplotlib.colors", colors) + # Use a temporary module name so stubbed imports cannot leak into later tests. + name = "_burr_streamlit_under_test" + spec = importlib.util.spec_from_file_location( + name, Path(__file__).resolve().parents[2] / "burr/integrations/streamlit.py" ) - streamlit = importlib.import_module("burr.integrations.streamlit") + streamlit = importlib.util.module_from_spec(spec) + monkeypatch.setitem(sys.modules, name, streamlit) + spec.loader.exec_module(streamlit) log_file = tmp_path / "state.jsonl" log_file.write_text( @@ -49,10 +60,13 @@ def test_load_state_from_log_file_reads_utf8(monkeypatch, tmp_path): ) real_open = builtins.open + opened = [] def guarded_open(*args, **kwargs): assert kwargs.get("encoding") == "utf-8" - return real_open(*args, **kwargs) + handle = real_open(*args, **kwargs) + opened.append(handle) + return handle monkeypatch.setattr(builtins, "open", guarded_open) @@ -63,3 +77,4 @@ def guarded_open(*args, **kwargs): assert state.history[0].state == {"message": "café"} assert state.history[0].action == "say" assert state.history[0].result == {"ok": True} + assert opened and all(handle.closed for handle in opened) From 07de89a2a529090f688d3b1a8905e1720734a1cd Mon Sep 17 00:00:00 2001 From: Ghraven Date: Tue, 6 Oct 2026 02:49:39 +0800 Subject: [PATCH 3/3] test: explicitly import importlib.util --- tests/integrations/test_streamlit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/test_streamlit.py b/tests/integrations/test_streamlit.py index 149084aab..6f1a3b8a6 100644 --- a/tests/integrations/test_streamlit.py +++ b/tests/integrations/test_streamlit.py @@ -16,7 +16,7 @@ # under the License. import builtins -import importlib +import importlib.util import json import sys import types