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:** 1. Copy template: .. code-block:: bash cp src/pyhnhtools/gui/std_step_1d_gui.py src/pyhnhtools/gui/my_module_gui.py 2. Rename class: .. code-block:: python class MyModuleInterface: pass 3. Adapt methods for your workflow 4. 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** .. code-block:: python 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** .. code-block:: python 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** .. code-block:: python 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** .. code-block:: python 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** .. code-block:: python 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** .. code-block:: python 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: .. code-block:: python 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: .. code-block:: python def run_solver(self) -> bool def new_model(self) -> bool def analyze_inlet(self) -> bool # Module-specific def update_results(self) -> None Docstring Template ------------------ .. code-block:: python 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 --------------- .. code-block:: python 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: .. code-block:: python 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 --------------- .. code-block:: python # 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 .. code-block:: python # WRONG - don't do this in adapter from PyQt5.QtWidgets import QMessageBox ✅ **Do**: Keep adapter GUI-agnostic .. code-block:: python # RIGHT - adapter has no GUI imports def run_analysis(model): return compute(model) ❌ **Don't**: Have solver import GUI .. code-block:: python # WRONG - solver shouldn't know about GUI def solve(self): self.gui.update() # NO! ✅ **Do**: Let interface handle updates .. code-block:: python # RIGHT - interface coordinates communication def run_solver(self): results = self.solver.compute() self._update_gui(results) ❌ **Don't**: Duplicate method signatures .. code-block:: python # WRONG - inconsistent interfaces class Interface1: def run_analysis(self): pass class Interface2: def execute_solver(self): pass ✅ **Do**: Follow consistent patterns .. code-block:: python # RIGHT - consistent method names class Interface1: def run_solver(self): pass class Interface2: def run_solver(self): pass See Also -------- - :doc:`module_integration` - Step-by-step guide - :doc:`contributing` - Contributing workflow - [std_step_1d_gui.py](../../api/gui.html) - Complete example - [GUI Tutorial](../../getting_started/gui_tutorial.html) - User guide