Skip to content

Commit fc7b8eb

Browse files
Copilotlachlangrose
andcommitted
feat: add debug manager and directory setting
Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com>
1 parent ae425a0 commit fc7b8eb

12 files changed

Lines changed: 466 additions & 27 deletions

loopstructural/debug_manager.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#! python3
2+
3+
"""Debug manager handling logging and debug directory management."""
4+
5+
# standard
6+
import datetime
7+
import json
8+
import os
9+
import tempfile
10+
import uuid
11+
from pathlib import Path
12+
from typing import Any
13+
14+
# PyQGIS
15+
from qgis.core import QgsProject
16+
17+
# project
18+
import loopstructural.toolbelt.preferences as plg_prefs_hdlr
19+
20+
21+
class DebugManager:
22+
"""Manage debug mode state, logging and debug file storage."""
23+
24+
def __init__(self, plugin):
25+
self.plugin = plugin
26+
self._session_dir = None
27+
self._session_id = uuid.uuid4().hex
28+
self._project_name = self._get_project_name()
29+
self._debug_state_logged = False
30+
31+
def _get_settings(self):
32+
return plg_prefs_hdlr.PlgOptionsManager.get_plg_settings()
33+
34+
def _get_project_name(self) -> str:
35+
try:
36+
proj = QgsProject.instance()
37+
title = proj.title()
38+
if title:
39+
return title
40+
stem = Path(proj.fileName() or "").stem
41+
return stem or "untitled_project"
42+
except Exception as err:
43+
self.plugin.log(
44+
message=f"[map2loop] Failed to resolve project name: {err}",
45+
log_level=1,
46+
)
47+
return "unknown_project"
48+
49+
def is_debug(self) -> bool:
50+
"""Return whether debug mode is enabled."""
51+
try:
52+
state = bool(self._get_settings().debug_mode)
53+
if not self._debug_state_logged:
54+
self.plugin.log(
55+
message=f"[map2loop] Debug mode: {'ON' if state else 'OFF'}",
56+
log_level=0,
57+
)
58+
self._debug_state_logged = True
59+
return state
60+
except Exception as err:
61+
self.plugin.log(
62+
message=f"[map2loop] Error checking debug mode: {err}",
63+
log_level=2,
64+
)
65+
return False
66+
67+
def get_effective_debug_dir(self) -> Path:
68+
"""Return the session debug directory, creating it if needed."""
69+
if self._session_dir is not None:
70+
return self._session_dir
71+
72+
try:
73+
debug_dir_pref = plg_prefs_hdlr.PlgOptionsManager.get_debug_directory()
74+
except Exception as err:
75+
self.plugin.log(
76+
message=f"[map2loop] Reading debug_directory failed: {err}",
77+
log_level=1,
78+
)
79+
debug_dir_pref = ""
80+
81+
base_dir = (
82+
Path(debug_dir_pref).expanduser()
83+
if str(debug_dir_pref).strip()
84+
else Path(tempfile.gettempdir()) / "map2loop_debug"
85+
)
86+
87+
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
88+
session_dir = base_dir / self._project_name / f"session_{self._session_id}_{ts}"
89+
90+
try:
91+
session_dir.mkdir(parents=True, exist_ok=True)
92+
except Exception as err:
93+
self.plugin.log(
94+
message=(
95+
f"[map2loop] Failed to create session dir '{session_dir}': {err}. "
96+
"Falling back to system temp."
97+
),
98+
log_level=1,
99+
)
100+
fallback = (
101+
Path(tempfile.gettempdir())
102+
/ "map2loop_debug"
103+
/ self._project_name
104+
/ f"session_{self._session_id}_{ts}"
105+
)
106+
try:
107+
fallback.mkdir(parents=True, exist_ok=True)
108+
except Exception as err_fallback:
109+
self.plugin.log(
110+
message=(
111+
f"[map2loop] Failed to create fallback debug dir '{fallback}': "
112+
f"{err_fallback}"
113+
),
114+
log_level=2,
115+
)
116+
fallback = Path(tempfile.gettempdir())
117+
session_dir = fallback
118+
119+
self._session_dir = session_dir
120+
self.plugin.log(
121+
message=f"[map2loop] Debug directory resolved: {session_dir}",
122+
log_level=0,
123+
)
124+
return self._session_dir
125+
126+
def log_params(self, context_label: str, params: Any):
127+
"""Log parameters and persist them when debug mode is enabled."""
128+
try:
129+
self.plugin.log(
130+
message=f"[map2loop] {context_label} parameters: {params}",
131+
log_level=0,
132+
)
133+
except Exception as err:
134+
self.plugin.log(
135+
message=(
136+
f"[map2loop] {context_label} parameters (stringified due to {err}): {params}"
137+
),
138+
log_level=0,
139+
)
140+
141+
if self.is_debug():
142+
try:
143+
debug_dir = self.get_effective_debug_dir()
144+
safe_label = context_label.replace(" ", "_").lower()
145+
file_path = debug_dir / f"{safe_label}_params.json"
146+
payload = params if isinstance(params, dict) else {"_payload": params}
147+
with open(file_path, "w", encoding="utf-8") as file_handle:
148+
json.dump(payload, file_handle, ensure_ascii=False, indent=2, default=str)
149+
self.plugin.log(
150+
message=f"[map2loop] Params saved to: {file_path}",
151+
log_level=0,
152+
)
153+
except Exception as err:
154+
self.plugin.log(
155+
message=f"[map2loop] Failed to save params for {context_label}: {err}",
156+
log_level=2,
157+
)
158+
159+
def save_debug_file(self, filename: str, content_bytes: bytes):
160+
"""Persist a debug file atomically and log its location."""
161+
try:
162+
debug_dir = self.get_effective_debug_dir()
163+
out_path = debug_dir / filename
164+
tmp_path = debug_dir / (filename + ".tmp")
165+
with open(tmp_path, "wb") as file_handle:
166+
file_handle.write(content_bytes)
167+
os.replace(tmp_path, out_path)
168+
self.plugin.log(
169+
message=f"[map2loop] Debug file saved: {out_path}",
170+
log_level=0,
171+
)
172+
return out_path
173+
except Exception as err:
174+
self.plugin.log(
175+
message=f"[map2loop] Failed to save debug file '{filename}': {err}",
176+
log_level=2,
177+
)
178+
return None

loopstructural/gui/dlg_settings.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ def __init__(self, parent):
7676
self.btn_reset.setIcon(QIcon(QgsApplication.iconPath("mActionUndo.svg")))
7777
self.btn_reset.pressed.connect(self.reset_settings)
7878

79+
if hasattr(self, "btn_browse_debug_directory"):
80+
self.btn_browse_debug_directory.pressed.connect(self._browse_debug_directory)
81+
if hasattr(self, "btn_open_debug_directory"):
82+
self.btn_open_debug_directory.pressed.connect(self._open_debug_directory)
83+
7984
# load previously saved settings
8085
self.load_settings()
8186

@@ -91,6 +96,9 @@ def apply(self):
9196
settings.interpolator_cpw = self.cpw_spin_box.value()
9297
settings.interpolator_regularisation = self.regularisation_spin_box.value()
9398
settings.version = __version__
99+
debug_dir_text = (self.le_debug_directory.text() if hasattr(self, "le_debug_directory") else "") or ""
100+
self.plg_settings.set_debug_directory(debug_dir_text)
101+
settings.debug_directory = debug_dir_text
94102

95103
# dump new settings into QgsSettings
96104
self.plg_settings.save_from_object(settings)
@@ -114,6 +122,8 @@ def load_settings(self):
114122
self.regularisation_spin_box.setValue(settings.interpolator_regularisation)
115123
self.cpw_spin_box.setValue(settings.interpolator_cpw)
116124
self.npw_spin_box.setValue(settings.interpolator_npw)
125+
if hasattr(self, "le_debug_directory"):
126+
self.le_debug_directory.setText(settings.debug_directory or "")
117127

118128
def reset_settings(self):
119129
"""Reset settings in the UI and persisted settings to plugin defaults."""
@@ -125,6 +135,23 @@ def reset_settings(self):
125135
# update the form
126136
self.load_settings()
127137

138+
def _browse_debug_directory(self):
139+
"""Open a directory selector for debug directory."""
140+
from qgis.PyQt.QtWidgets import QFileDialog
141+
142+
start_dir = (self.le_debug_directory.text() if hasattr(self, "le_debug_directory") else "") or ""
143+
chosen = QFileDialog.getExistingDirectory(self, "Select Debug Files Directory", start_dir)
144+
if chosen and hasattr(self, "le_debug_directory"):
145+
self.le_debug_directory.setText(chosen)
146+
147+
def _open_debug_directory(self):
148+
"""Open configured debug directory in the system file manager."""
149+
target = self.plg_settings.get_debug_directory() or ""
150+
if target:
151+
QDesktopServices.openUrl(QUrl.fromLocalFile(target))
152+
else:
153+
self.log(message="[map2loop] No debug directory configured.", log_level=1)
154+
128155

129156
class PlgOptionsFactory(QgsOptionsWidgetFactory):
130157
"""Factory for options widget."""

loopstructural/gui/dlg_settings.ui

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@
7878
<bool>false</bool>
7979
</property>
8080
<layout class="QGridLayout" name="gridLayout">
81-
<item row="8" column="0" colspan="2">
82-
<widget class="QPushButton" name="btn_reset">
83-
<property name="minimumSize">
81+
<item row="9" column="0" colspan="2">
82+
<widget class="QPushButton" name="btn_reset">
83+
<property name="minimumSize">
8484
<size>
8585
<width>200</width>
8686
<height>25</height>
@@ -100,8 +100,8 @@
100100
</property>
101101
</widget>
102102
</item>
103-
<item row="7" column="1">
104-
<widget class="QLabel" name="lbl_version_saved_value">
103+
<item row="7" column="1">
104+
<widget class="QLabel" name="lbl_version_saved_value">
105105
<property name="minimumSize">
106106
<size>
107107
<width>0</width>
@@ -147,8 +147,36 @@
147147
</property>
148148
</widget>
149149
</item>
150-
<item row="0" column="0">
151-
<widget class="QPushButton" name="btn_help">
150+
<item row="8" column="0">
151+
<widget class="QLabel" name="lbl_debug_directory">
152+
<property name="text">
153+
<string>Debug directory</string>
154+
</property>
155+
</widget>
156+
</item>
157+
<item row="8" column="1">
158+
<layout class="QHBoxLayout" name="horizontalLayout">
159+
<item>
160+
<widget class="QLineEdit" name="le_debug_directory"/>
161+
</item>
162+
<item>
163+
<widget class="QPushButton" name="btn_browse_debug_directory">
164+
<property name="text">
165+
<string>Browse...</string>
166+
</property>
167+
</widget>
168+
</item>
169+
<item>
170+
<widget class="QPushButton" name="btn_open_debug_directory">
171+
<property name="text">
172+
<string>Open Folder</string>
173+
</property>
174+
</widget>
175+
</item>
176+
</layout>
177+
</item>
178+
<item row="0" column="0">
179+
<widget class="QPushButton" name="btn_help">
152180
<property name="minimumSize">
153181
<size>
154182
<width>200</width>

loopstructural/gui/map2loop_tools/basal_contacts_widget.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class BasalContactsWidget(QWidget):
1717
from geology layers.
1818
"""
1919

20-
def __init__(self, parent=None, data_manager=None):
20+
def __init__(self, parent=None, data_manager=None, debug_manager=None):
2121
"""Initialize the basal contacts widget.
2222
2323
Parameters
@@ -29,6 +29,7 @@ def __init__(self, parent=None, data_manager=None):
2929
"""
3030
super().__init__(parent)
3131
self.data_manager = data_manager
32+
self._debug = debug_manager
3233

3334
# Load the UI file
3435
ui_path = os.path.join(os.path.dirname(__file__), "basal_contacts_widget.ui")
@@ -62,6 +63,17 @@ def __init__(self, parent=None, data_manager=None):
6263
# Set up field combo boxes
6364
self._setup_field_combo_boxes()
6465

66+
def set_debug_manager(self, debug_manager):
67+
"""Attach a debug manager instance."""
68+
self._debug = debug_manager
69+
70+
def _log_params(self, context_label: str):
71+
if getattr(self, "_debug", None):
72+
try:
73+
self._debug.log_params(context_label=context_label, params=self.get_parameters())
74+
except Exception:
75+
pass
76+
6577
def _guess_layers(self):
6678
"""Attempt to auto-select layers based on common naming conventions."""
6779
if not self.data_manager:
@@ -113,6 +125,8 @@ def _on_geology_layer_changed(self):
113125

114126
def _run_extractor(self):
115127
"""Run the basal contacts extraction algorithm."""
128+
self._log_params("basal_contacts_widget_run")
129+
116130
# Validate inputs
117131
if not self.geologyLayerComboBox.currentLayer():
118132
QMessageBox.warning(self, "Missing Input", "Please select a geology layer.")
@@ -160,6 +174,16 @@ def _run_extractor(self):
160174
"Success",
161175
f"Successfully extracted {contact_type}!",
162176
)
177+
if self._debug and self._debug.is_debug():
178+
try:
179+
self._debug.save_debug_file(
180+
"basal_contacts_result.txt", str(result).encode("utf-8")
181+
)
182+
except Exception as err:
183+
self._debug.plugin.log(
184+
message=f"[map2loop] Failed to save basal contacts debug output: {err}",
185+
log_level=2,
186+
)
163187

164188
def get_parameters(self):
165189
"""Get current widget parameters.

0 commit comments

Comments
 (0)