Skip to content

Commit 98ca1fb

Browse files
clanker-loverclaude
andcommitted
Refactor remaining flagged items for clean self-analysis
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 45a9503 commit 98ca1fb

3 files changed

Lines changed: 143 additions & 153 deletions

File tree

codedocent/editor.py

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,41 @@
66
import shutil
77

88

9+
def _read_and_validate(
10+
filepath: str, start_line: int, end_line: int,
11+
) -> tuple[list[str] | None, str | None]:
12+
"""Read *filepath* and validate the line range.
13+
14+
Returns ``(lines, None)`` on success, or ``(None, error_message)``
15+
on failure.
16+
"""
17+
if not os.path.isfile(filepath):
18+
return (None, f"File not found: {filepath}")
19+
if (
20+
not isinstance(start_line, int)
21+
or not isinstance(end_line, int)
22+
or start_line < 1
23+
or end_line < 1
24+
or start_line > end_line
25+
):
26+
return (None, f"Invalid line range: {start_line}-{end_line}")
27+
with open(filepath, encoding="utf-8") as f:
28+
lines = f.readlines()
29+
if end_line > len(lines):
30+
return (
31+
None,
32+
f"end_line {end_line} exceeds file length ({len(lines)} lines)",
33+
)
34+
return (lines, None)
35+
36+
37+
def _write_with_backup(filepath: str, lines: list[str]) -> None:
38+
"""Create a ``.bak`` backup and write *lines* back to *filepath*."""
39+
shutil.copy2(filepath, filepath + ".bak")
40+
with open(filepath, "w", encoding="utf-8") as f:
41+
f.writelines(lines)
42+
43+
944
def replace_block_source(
1045
filepath: str,
1146
start_line: int,
@@ -18,45 +53,16 @@ def replace_block_source(
1853
``success``, ``lines_before``, ``lines_after`` on success, or
1954
``success=False`` and ``error`` on failure.
2055
"""
21-
# --- input validation ---
22-
if not os.path.isfile(filepath):
23-
return {"success": False, "error": f"File not found: {filepath}"}
24-
25-
if (
26-
not isinstance(start_line, int)
27-
or not isinstance(end_line, int)
28-
or start_line < 1
29-
or end_line < 1
30-
or start_line > end_line
31-
):
32-
return {
33-
"success": False,
34-
"error": (
35-
f"Invalid line range: {start_line}-{end_line}"
36-
),
37-
}
38-
3956
if not isinstance(new_source, str):
4057
return {"success": False, "error": "new_source must be a string"}
4158

42-
try:
43-
with open(filepath, encoding="utf-8") as f:
44-
lines = f.readlines()
45-
46-
if end_line > len(lines):
47-
return {
48-
"success": False,
49-
"error": (
50-
f"end_line {end_line} exceeds file length"
51-
f" ({len(lines)} lines)"
52-
),
53-
}
59+
lines, error = _read_and_validate(filepath, start_line, end_line)
60+
if lines is None:
61+
return {"success": False, "error": error}
5462

55-
old_count = end_line - start_line + 1
56-
57-
# Backup
58-
shutil.copy2(filepath, filepath + ".bak")
63+
old_count = end_line - start_line + 1
5964

65+
try:
6066
# Build replacement lines
6167
if new_source == "":
6268
new_lines: list[str] = []
@@ -69,11 +75,9 @@ def replace_block_source(
6975
new_lines = [ln + "\n" for ln in new_lines]
7076

7177
new_count = len(new_lines)
72-
7378
lines[start_line - 1:end_line] = new_lines
7479

75-
with open(filepath, "w", encoding="utf-8") as f:
76-
f.writelines(lines)
80+
_write_with_backup(filepath, lines)
7781

7882
return {
7983
"success": True,

codedocent/gui.py

Lines changed: 47 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,15 @@
2020
_fetch_ollama_models = fetch_ollama_models
2121

2222

23-
def _build_gui() -> None:
24-
"""Build and run the tkinter GUI window."""
25-
root = tk.Tk()
26-
root.title("codedocent")
27-
root.resizable(False, False)
28-
29-
frame = ttk.Frame(root, padding=16)
30-
frame.grid(row=0, column=0, sticky="nsew")
31-
32-
# --- Folder picker ---
23+
def _create_folder_row(frame: ttk.Frame) -> tk.StringVar:
24+
"""Create the folder-picker row and return the StringVar."""
3325
ttk.Label(frame, text="Folder to analyze:").grid(
3426
row=0, column=0, sticky="w", pady=(0, 4),
3527
)
36-
3728
folder_var = tk.StringVar()
38-
folder_entry = ttk.Entry(frame, textvariable=folder_var, width=40)
39-
folder_entry.grid(row=1, column=0, sticky="ew", padx=(0, 6))
29+
ttk.Entry(frame, textvariable=folder_var, width=40).grid(
30+
row=1, column=0, sticky="ew", padx=(0, 6),
31+
)
4032

4133
def _browse() -> None:
4234
path = filedialog.askdirectory()
@@ -46,46 +38,50 @@ def _browse() -> None:
4638
ttk.Button(frame, text="Browse...", command=_browse).grid(
4739
row=1, column=1, sticky="w",
4840
)
41+
return folder_var
42+
4943

50-
# --- Model dropdown ---
44+
def _create_model_row(frame: ttk.Frame) -> tk.StringVar:
45+
"""Create the model-dropdown row and return the StringVar."""
5146
ttk.Label(frame, text="Model:").grid(
5247
row=2, column=0, sticky="w", pady=(12, 4),
5348
)
54-
5549
ollama_ok = _check_ollama()
5650
models = _fetch_ollama_models() if ollama_ok else []
57-
5851
model_values = models if models else ["No AI"]
5952
model_var = tk.StringVar(value=model_values[0])
60-
model_combo = ttk.Combobox(
53+
ttk.Combobox(
6154
frame, textvariable=model_var, values=model_values,
6255
state="readonly", width=37,
63-
)
64-
model_combo.grid(row=3, column=0, columnspan=2, sticky="ew")
56+
).grid(row=3, column=0, columnspan=2, sticky="ew")
57+
return model_var
6558

66-
# --- Mode selector ---
59+
60+
def _create_mode_row(frame: ttk.Frame) -> tk.StringVar:
61+
"""Create the mode-selector row and return the StringVar."""
6762
ttk.Label(frame, text="Mode:").grid(
6863
row=4, column=0, sticky="w", pady=(12, 4),
6964
)
70-
7165
mode_var = tk.StringVar(value="interactive")
7266
modes_frame = ttk.Frame(frame)
7367
modes_frame.grid(row=5, column=0, columnspan=2, sticky="w")
74-
75-
ttk.Radiobutton(
76-
modes_frame, text="Interactive", variable=mode_var,
77-
value="interactive",
78-
).pack(anchor="w")
79-
ttk.Radiobutton(
80-
modes_frame, text="Full export", variable=mode_var,
81-
value="full",
82-
).pack(anchor="w")
83-
ttk.Radiobutton(
84-
modes_frame, text="Text tree", variable=mode_var,
85-
value="text",
86-
).pack(anchor="w")
87-
88-
# --- Go button ---
68+
for text, value in [("Interactive", "interactive"),
69+
("Full export", "full"),
70+
("Text tree", "text")]:
71+
ttk.Radiobutton(
72+
modes_frame, text=text, variable=mode_var, value=value,
73+
).pack(anchor="w")
74+
return mode_var
75+
76+
77+
def _create_go_button(
78+
frame: ttk.Frame,
79+
root: tk.Tk,
80+
folder_var: tk.StringVar,
81+
model_var: tk.StringVar,
82+
mode_var: tk.StringVar,
83+
) -> None:
84+
"""Create the Go button with its launch logic."""
8985
def _go() -> None:
9086
folder = folder_var.get().strip()
9187
if not folder:
@@ -112,6 +108,21 @@ def _go() -> None:
112108
row=6, column=0, columnspan=2, pady=(16, 0),
113109
)
114110

111+
112+
def _build_gui() -> None:
113+
"""Build and run the tkinter GUI window."""
114+
root = tk.Tk()
115+
root.title("codedocent")
116+
root.resizable(False, False)
117+
118+
frame = ttk.Frame(root, padding=16)
119+
frame.grid(row=0, column=0, sticky="nsew")
120+
121+
folder_var = _create_folder_row(frame)
122+
model_var = _create_model_row(frame)
123+
mode_var = _create_mode_row(frame)
124+
_create_go_button(frame, root, folder_var, model_var, mode_var)
125+
115126
root.mainloop()
116127

117128

codedocent/server.py

Lines changed: 55 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,52 @@ def _idle_watcher():
120120
watcher.start()
121121

122122

123+
def _analyze_node(node_id: str) -> dict | None:
124+
"""Look up *node_id* and return an analyzed dict, or None if not found."""
125+
if node_id not in _Handler.node_lookup:
126+
return None
127+
node = _Handler.node_lookup[node_id]
128+
if node.summary is not None:
129+
return _node_to_dict(node, include_source=True)
130+
with _Handler.analyze_lock:
131+
if node.summary is None:
132+
from codedocent.analyzer import analyze_single_node # pylint: disable=import-outside-toplevel # noqa: E501
133+
134+
analyze_single_node(node, _Handler.model, _Handler.cache_dir)
135+
return _node_to_dict(node, include_source=True)
136+
137+
138+
def _execute_replace(node_id: str, body: dict) -> tuple[int, dict]:
139+
"""Validate and execute a source-replacement request.
140+
141+
Returns ``(status_code, result_dict)``.
142+
"""
143+
if node_id not in _Handler.node_lookup:
144+
return (404, {"success": False, "error": "Unknown node ID"})
145+
node = _Handler.node_lookup[node_id]
146+
if node.node_type in ("directory", "file"):
147+
return (
148+
400,
149+
{"success": False,
150+
"error": "Cannot replace directory/file blocks"},
151+
)
152+
new_source = body.get("source", "")
153+
if not isinstance(new_source, str):
154+
return (400, {"success": False, "error": "source must be a string"})
155+
abs_path = _resolve_filepath(node, _Handler.cache_dir)
156+
from codedocent.editor import replace_block_source # pylint: disable=import-outside-toplevel # noqa: E501
157+
158+
with _Handler.analyze_lock:
159+
result = replace_block_source(
160+
abs_path, node.start_line, node.end_line, new_source,
161+
)
162+
if result["success"]:
163+
_update_node_after_replace(
164+
node, new_source, result, _Handler.cache_dir,
165+
)
166+
return (200, result)
167+
168+
123169
class _Handler(BaseHTTPRequestHandler):
124170
"""HTTP request handler for codedocent server."""
125171

@@ -175,101 +221,30 @@ def _serve_html(self):
175221
self.wfile.write(data)
176222

177223
def _serve_tree(self):
178-
tree_dict = _node_to_dict(_Handler.root)
179-
data = json.dumps(tree_dict).encode("utf-8")
180-
self.send_response(200)
181-
self.send_header("Content-Type", "application/json")
182-
self.send_header("Content-Length", str(len(data)))
183-
self.end_headers()
184-
self.wfile.write(data)
224+
self._send_json(200, _node_to_dict(_Handler.root))
185225

186226
def _handle_source(self, node_id: str):
187227
if node_id not in _Handler.node_lookup:
188228
self.send_error(404, "Unknown node ID")
189229
return
190230
node = _Handler.node_lookup[node_id]
191-
result = {"source": node.source or ""}
192-
data = json.dumps(result).encode("utf-8")
193-
self.send_response(200)
194-
self.send_header("Content-Type", "application/json")
195-
self.send_header("Content-Length", str(len(data)))
196-
self.end_headers()
197-
self.wfile.write(data)
231+
self._send_json(200, {"source": node.source or ""})
198232

199233
def _handle_analyze(self, node_id: str):
200-
if node_id not in _Handler.node_lookup:
234+
result = _analyze_node(node_id)
235+
if result is None:
201236
self.send_error(404, "Unknown node ID")
202237
return
203-
204-
node = _Handler.node_lookup[node_id]
205-
206-
# Return cached result if already analyzed
207-
if node.summary is not None:
208-
result = _node_to_dict(node, include_source=True)
209-
data = json.dumps(result).encode("utf-8")
210-
self.send_response(200)
211-
self.send_header("Content-Type", "application/json")
212-
self.send_header("Content-Length", str(len(data)))
213-
self.end_headers()
214-
self.wfile.write(data)
215-
return
216-
217-
# Run analysis (thread-safe)
218-
with _Handler.analyze_lock:
219-
# Double-check after acquiring lock
220-
if node.summary is None:
221-
from codedocent.analyzer import analyze_single_node # pylint: disable=import-outside-toplevel # noqa: E501
222-
223-
analyze_single_node(node, _Handler.model, _Handler.cache_dir)
224-
225-
result = _node_to_dict(node, include_source=True)
226-
data = json.dumps(result).encode("utf-8")
227-
self.send_response(200)
228-
self.send_header("Content-Type", "application/json")
229-
self.send_header("Content-Length", str(len(data)))
230-
self.end_headers()
231-
self.wfile.write(data)
238+
self._send_json(200, result)
232239

233240
def _handle_replace(self, node_id: str):
234-
if node_id not in _Handler.node_lookup:
235-
self.send_error(404, "Unknown node ID")
236-
return
237-
238-
node = _Handler.node_lookup[node_id]
239-
240-
if node.node_type in ("directory", "file"):
241-
self._send_json(
242-
400,
243-
{"success": False,
244-
"error": "Cannot replace directory/file blocks"},
245-
)
246-
return
247-
248241
content_length = int(self.headers["Content-Length"])
249242
body = json.loads(self.rfile.read(content_length))
250-
new_source = body.get("source", "")
251-
252-
if not isinstance(new_source, str):
253-
self._send_json(
254-
400,
255-
{"success": False, "error": "source must be a string"},
256-
)
243+
status, result = _execute_replace(node_id, body)
244+
if status == 404:
245+
self.send_error(404, result["error"])
257246
return
258-
259-
abs_path = _resolve_filepath(node, _Handler.cache_dir)
260-
261-
from codedocent.editor import replace_block_source # pylint: disable=import-outside-toplevel # noqa: E501
262-
263-
with _Handler.analyze_lock:
264-
result = replace_block_source(
265-
abs_path, node.start_line, node.end_line, new_source,
266-
)
267-
if result["success"]:
268-
_update_node_after_replace(
269-
node, new_source, result, _Handler.cache_dir,
270-
)
271-
272-
self._send_json(200, result)
247+
self._send_json(status, result)
273248

274249
def _send_json(self, status_code: int, obj: dict):
275250
data = json.dumps(obj).encode("utf-8")

0 commit comments

Comments
 (0)