Templates & Patterns
Reference template files and code patterns for extending pyhnhtools.
Template Files
All template files are located in src/pyhnhtools/gui/:
- std_step_1d_gui.py
Complete working example of module integration
File-based, geometry-driven workflow
Shows all required methods
Use as reference or starting point
- culverts_gui.py
Alternative pattern for parameter-based modules
Parameter-based analysis (not file-based)
Different workflow orchestration
Shows workflow variation
- MODULE_INTEGRATION_GUIDE.md
Comprehensive architecture documentation
Three-layer pattern explained
Step-by-step instructions
Design principles
- IMPLEMENTATION_CHECKLIST.md
Practical workflow checklist
Phase-by-phase tasks
Pre-built code snippets
Troubleshooting guide
Using Templates
To Create a New Module GUI:
Copy template:
cp src/pyhnhtools/gui/std_step_1d_gui.py src/pyhnhtools/gui/my_module_gui.py
Rename class:
class MyModuleInterface: pass
Adapt methods for your workflow
Register in qt_gui.py
When to Use Which Template:
std_step_1d_gui.py: File-based models (geometry, reach data)
culverts_gui.py: Parameter-based analysis (form inputs)
Code Patterns
Pattern 1: Initialization
class MyModuleInterface:
def __init__(self, gui_widget):
self.gui = gui_widget
self.model = None
self.results = None
def setup(self) -> bool:
try:
from pyhnhtools.my_module import MyClass
self.MyClass = MyClass
return True
except ImportError as e:
self.gui.text_out.setText(f"Error: {e}")
return False
Pattern 2: Workflow Methods
def load_model(self, filepath):
# Load from file
with open(filepath) as f:
data = json.load(f)
self.model = self.ModelClass(**data)
def run_solver(self):
if not self.validate_model():
return False
try:
self.results = compute(self.model)
self._update_display()
return True
except Exception as e:
self.gui.text_out.setText(f"Error: {e}")
return False
Pattern 3: Error Handling
try:
result = risky_operation()
except ValueError as e:
logger.error(f"Validation error: {e}")
self.gui.text_out.setText(f"Error: {e}")
return False
except IOError as e:
logger.error(f"File error: {e}")
self.gui.text_out.setText(f"File error: {e}")
return False
Pattern 4: GUI Updates
def _update_display(self):
# Update table
self.gui.table_results.setRowCount(len(self.data))
for row, item in enumerate(self.data):
self.gui.table_results.setItem(row, 0, QTableWidgetItem(str(item)))
# Update text
self.gui.text_out.setText("Analysis complete!")
# Update plots
self._generate_plots()
Pattern 5: Validation
def validate_model(self) -> bool:
if not self.model:
self.gui.text_out.setText("No model loaded")
return False
if len(self.model.data) == 0:
self.gui.text_out.setText("Model is empty")
return False
if self.model.parameter < 0:
self.gui.text_out.setText("Invalid parameter")
return False
return True
Pattern 6: Plotting
def _generate_plots(self):
import plotly.graph_objects as go
fig = go.Figure()
fig.add_trace(go.Scatter(
x=self.results.x,
y=self.results.y,
mode='lines',
name='Result'
))
fig.write_html("plot.html")
self.gui.plot_widget.setHtml(open("plot.html").read())
Method Signatures
Required Methods
All modules should implement:
class {Module}Interface:
def __init__(self, gui_widget)
def setup(self) -> bool
def validate_model(self) -> bool
def save_model(self, filepath: str) -> bool
def load_model(self, filepath: str) -> bool
Optional Methods
May vary by module:
def run_solver(self) -> bool
def new_model(self) -> bool
def analyze_inlet(self) -> bool # Module-specific
def update_results(self) -> None
Docstring Template
def my_method(self, param1: str, param2: int = 10) -> bool:
"""
Brief one-line description.
Longer explanation if needed. Explain what the method does,
how it works, and any important details.
Args:
param1: Description of first parameter
param2: Description of second parameter (default: 10)
Returns:
True if successful, False otherwise
Raises:
ValueError: If parameter validation fails
IOError: If file operations fail
Example:
>>> result = my_method("input", param2=20)
>>> assert result == True
"""
Logging Pattern
import logging
logger = logging.getLogger(__name__)
class MyInterface:
def setup(self):
try:
# operation
logger.info("Setup completed successfully")
return True
except Exception as e:
logger.error(f"Setup failed: {e}")
return False
def run_solver(self):
logger.debug("Starting solver")
try:
results = self.solver.compute()
logger.info(f"Solver completed with {len(results)} results")
return True
except Exception as e:
logger.error(f"Solver error: {e}")
return False
GUI Integration Pattern
In qt_gui.py:
from pyhnhtools.gui.my_module_gui import MyModuleInterface
class BackwaterWidget(QMainWindow):
def __init__(self):
# Initialize interface
self.my_module = MyModuleInterface(self)
self.my_module.setup()
# Create tab
self.create_my_module_tab()
def create_my_module_tab(self):
tab = QWidget()
layout = QVBoxLayout()
# Add controls
self.tabs.addTab(tab, "My Module")
def on_run_clicked(self):
if self.current_tab == MY_MODULE_INDEX:
self.my_module.run_solver()
Testing Pattern
# tests/test_my_module_gui.py
import pytest
from unittest.mock import Mock
from pyhnhtools.gui.my_module_gui import MyModuleInterface
class TestMyModuleInterface:
def setup_method(self):
self.gui = Mock()
self.interface = MyModuleInterface(self.gui)
def test_setup(self):
assert self.interface.setup() == True
def test_load_model(self):
assert self.interface.load_model("test_model.json") == True
def test_validate_model(self):
self.interface.model = self.create_test_model()
assert self.interface.validate_model() == True
def test_run_solver(self):
assert self.interface.run_solver() == True
def create_test_model(self):
# Create minimal valid model
pass
Common Gotchas
❌ Don’t: Import PyQt5 in adapter layer
# WRONG - don't do this in adapter
from PyQt5.QtWidgets import QMessageBox
✅ Do: Keep adapter GUI-agnostic
# RIGHT - adapter has no GUI imports
def run_analysis(model):
return compute(model)
❌ Don’t: Have solver import GUI
# WRONG - solver shouldn't know about GUI
def solve(self):
self.gui.update() # NO!
✅ Do: Let interface handle updates
# RIGHT - interface coordinates communication
def run_solver(self):
results = self.solver.compute()
self._update_gui(results)
❌ Don’t: Duplicate method signatures
# WRONG - inconsistent interfaces
class Interface1:
def run_analysis(self): pass
class Interface2:
def execute_solver(self): pass
✅ Do: Follow consistent patterns
# RIGHT - consistent method names
class Interface1:
def run_solver(self): pass
class Interface2:
def run_solver(self): pass
See Also
Module Integration - Step-by-step guide
Contributing - Contributing workflow
[std_step_1d_gui.py](../../api/gui.html) - Complete example
[GUI Tutorial](../../getting_started/gui_tutorial.html) - User guide