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:

    cp src/pyhnhtools/gui/std_step_1d_gui.py src/pyhnhtools/gui/my_module_gui.py
    
  2. Rename class:

    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

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