BioGeek Claude Opus 4.7 (1M context) commited on
Commit
b132227
·
1 Parent(s): 59d0c68

Patch gradio_log.Log in-place for per-session tailing

Browse files

The SessionLog subclass introduced earlier caused 404s on its
templates/component/{index.js,style.css} when deployed to HF Spaces.
Reason: gradio/routes.py custom_component_path resolves frontend
assets relative to the component's __module__ file. Subclassing in
app.py made Gradio look for <app_dir>/templates/component/index.js,
which doesn't exist, so the bundle was never served and the SPA
hung in a loading loop.

Subclassing with __module__ = "gradio_log.log" also breaks because
Gradio's component_meta generates a .pyi stub at class-definition
time via inspect.getsource(), which then can't locate the source.

Monkey-patch Log.read_to_end / Log.get_current_reading_pos on the
upstream class instead. Log.__init__ captures value=self.read_to_end
at construction, so binding takes effect for a vanilla Log() call;
no subclass, no @gr .render, no asset-path drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +57 -49
app.py CHANGED
@@ -139,56 +139,64 @@ def _cleanup_session_log(path: str | None) -> None:
139
  Path(path).unlink(missing_ok=True)
140
 
141
 
142
- class SessionLog(Log):
143
- """gradio_log.Log variant that tails {LOG_DIR}/{session_hash}.log instead of
144
- a single shared file. The base class polls read_to_end(session_hash) on a
145
- timer (with the caller's own session_hash injected by Gradio), so deriving
146
- the file path from that argument gives perfect per-session isolation —
147
- without the @gr.render reshuffling that triggered KeyError: <fn_index> in
148
- Gradio's queue when the Log was being re-rendered.
149
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
- def _session_path(self, session_hash: str) -> Path:
152
- return LOG_DIR / f"{session_hash}.log"
153
-
154
- def _find_start_position(self, path: Path) -> int:
155
- with path.open("rb") as f:
156
- f.seek(0, 2)
157
- file_size = f.tell()
158
- lines_found = 0
159
- block_size = 1024
160
- blocks = []
161
- while f.tell() > 0 and lines_found <= self.tail:
162
- f.seek(max(f.tell() - block_size, 0))
163
- block = f.read(block_size)
164
- blocks.append(block)
165
- lines_found += block.count(b"\n")
166
- f.seek(-len(block), 1)
167
- all_read_bytes = b"".join(reversed(blocks))
168
- lines = all_read_bytes.splitlines()
169
- if self.tail >= len(lines):
170
- return 0
171
- last_lines = b"\n".join(lines[-self.tail:])
172
- return file_size - len(last_lines) - 1
173
-
174
- def get_current_reading_pos(self, session_hash: str) -> int:
175
- if session_hash not in self.current_reading_positions:
176
- path = self._session_path(session_hash)
177
- self.current_reading_positions[session_hash] = (
178
- self._find_start_position(path) if path.exists() else 0
179
- )
180
- return self.current_reading_positions[session_hash]
181
 
182
- def read_to_end(self, session_hash: str) -> str:
183
- path = self._session_path(session_hash)
184
- if not path.exists():
185
- return ""
186
- with path.open("rb") as f:
187
- current_pos = self.get_current_reading_pos(session_hash)
188
- f.seek(current_pos)
189
- data = f.read().decode()
190
- self.current_reading_positions[session_hash] = f.tell()
191
- return data
192
 
193
 
194
  def _resolve_device() -> None:
@@ -976,7 +984,7 @@ with gr.Blocks(
976
  session_log_path = gr.State(delete_callback=_cleanup_session_log)
977
 
978
  with gr.Accordion("Application Logs", open=True):
979
- SessionLog(str(_LOG_PLACEHOLDER), dark=True, height=300)
980
 
981
 
982
  gr.Markdown(""" **Links:**
 
139
  Path(path).unlink(missing_ok=True)
140
 
141
 
142
+ # Per-session tailing patched onto gradio_log.Log.
143
+ #
144
+ # Why monkey-patch the upstream class instead of subclassing:
145
+ # 1. @gr.render rebuilds the component on every state update, which shuffles
146
+ # Gradio's fn_index registry and triggers KeyError: <int> in the queue.
147
+ # 2. Subclassing Log inside app.py changes the component's __module__, and
148
+ # Gradio's custom-component asset route serves templates/component/{js,css}
149
+ # relative to that module's directory — so the bundle 404s on HF Spaces
150
+ # where <app_dir>/templates/component/ doesn't exist.
151
+ # 3. Log.__init__ captures `value=self.read_to_end` at construction, so
152
+ # patching the instance after the fact wouldn't change what the timer
153
+ # actually polls; we have to patch the class before any Log() call.
154
+ def _session_path(session_hash: str) -> Path:
155
+ return LOG_DIR / f"{session_hash}.log"
156
+
157
+
158
+ def _session_log_find_start(self: Log, path: Path) -> int:
159
+ with path.open("rb") as f:
160
+ f.seek(0, 2)
161
+ file_size = f.tell()
162
+ lines_found = 0
163
+ block_size = 1024
164
+ blocks: list[bytes] = []
165
+ while f.tell() > 0 and lines_found <= self.tail:
166
+ f.seek(max(f.tell() - block_size, 0))
167
+ block = f.read(block_size)
168
+ blocks.append(block)
169
+ lines_found += block.count(b"\n")
170
+ f.seek(-len(block), 1)
171
+ lines = b"".join(reversed(blocks)).splitlines()
172
+ if self.tail >= len(lines):
173
+ return 0
174
+ last_lines = b"\n".join(lines[-self.tail :])
175
+ return file_size - len(last_lines) - 1
176
+
177
+
178
+ def _session_log_get_pos(self: Log, session_hash: str) -> int:
179
+ if session_hash not in self.current_reading_positions:
180
+ path = _session_path(session_hash)
181
+ self.current_reading_positions[session_hash] = (
182
+ _session_log_find_start(self, path) if path.exists() else 0
183
+ )
184
+ return self.current_reading_positions[session_hash]
185
+
186
+
187
+ def _session_log_read_to_end(self: Log, session_hash: str) -> str:
188
+ path = _session_path(session_hash)
189
+ if not path.exists():
190
+ return ""
191
+ with path.open("rb") as f:
192
+ f.seek(_session_log_get_pos(self, session_hash))
193
+ data = f.read().decode()
194
+ self.current_reading_positions[session_hash] = f.tell()
195
+ return data
196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
 
198
+ Log.get_current_reading_pos = _session_log_get_pos
199
+ Log.read_to_end = _session_log_read_to_end
 
 
 
 
 
 
 
 
200
 
201
 
202
  def _resolve_device() -> None:
 
984
  session_log_path = gr.State(delete_callback=_cleanup_session_log)
985
 
986
  with gr.Accordion("Application Logs", open=True):
987
+ Log(str(_LOG_PLACEHOLDER), dark=True, height=300)
988
 
989
 
990
  gr.Markdown(""" **Links:**