Contributing to h5forest
We welcome contributions to h5forest! This guide will help you get started with contributing to the project.
Development Setup
Prerequisites
- Python 3.9 or higher
- Git
- HDF5 libraries (for
h5py) - pre-commit hook installation (see below)
Installation
- Fork and clone the repository:
- Install in development mode:
This installs h5forest along with development dependencies:
- ruff - Code linting and formatting
- pytest - Testing framework
- Additional testing utilities
- Install documentation tools (optional):
This enables local documentation building with:
- mkdocs - Documentation site generator
- mkdocs-material - Material theme for MkDocs
- Related plugins and extensions
- Install pre-commit hooks:
Pre-commit hooks automatically enforce our code quality standards. Pull requests that don't pass these checks will be rejected by our CI workflows.
The hooks perform the following checks: - Ruff linting and formatting - Ensures code style consistency (PEP 8, 79 char lines, etc.) - Merge conflict detection - Prevents accidental merge conflict markers - Large file prevention - Stops large files from being added - Case conflict checks - Avoids filename case issues across operating systems
Once installed, these checks run automatically on staged files before every commit, catching issues early and saving time in the review process.
You can also run them manually:
# Run on all files
pre-commit run --all-files
# Run on specific files
pre-commit run --files path/to/file.py
- Verify installation:
# Run tests
pytest
# Check linting
ruff check .
# Try the application
h5forest tests/fixtures/simple.h5
Development Workflow
Before Making Changes
- Create a new branch:
- Run tests to ensure everything works:
Making Changes
-
Make your changes following the coding standards below
-
Add tests for new functionality (see Testing Guide)
-
Run tests and linting:
# Run all tests
pytest
# Check code formatting
ruff check .
ruff format .
# Fix auto-fixable issues
ruff check --fix .
- Test your changes manually:
Submitting Changes
- Commit your changes:
- Push to your fork:
- Create a Pull Request on GitHub
Coding Standards
Code Style
We use ruff for code formatting and linting. All code must pass ruff checks before being merged.
Formatting Rules:
- Line length: Maximum 79 characters (PEP 8 compliant)
- Indentation: 4 spaces (no tabs)
- Quote style: Double quotes for strings (single quotes for dictionary keys when appropriate)
- Blank lines: Two blank lines between top-level definitions, one between methods
- Trailing whitespace: Not allowed
- Import sorting: Organized alphabetically with isort rules
Import Organization:
# Standard library imports
import os
import sys
from pathlib import Path
# Third-party imports
import h5py
import numpy as np
from prompt_toolkit import Application
# Local application imports
from h5forest.node import Node
from h5forest.tree import Tree
Naming Conventions:
- Classes:
PascalCase(e.g.,TreeProcessor,DatasetNode) - Functions/Methods:
snake_case(e.g.,process_data,get_metadata) - Constants:
UPPER_SNAKE_CASE(e.g.,MAX_DISPLAY_ITEMS,DEFAULT_CHUNK_SIZE) - Private members: Prefix with single underscore (e.g.,
_internal_method) - Variables:
snake_case(e.g.,file_path,node_count)
Code Quality
- Type hints: Strongly encouraged for function signatures and class attributes
- Docstrings: Required for all public functions, classes, and modules
- Comments: Use sparingly; prefer self-documenting code. When needed, explain why, not what
- Error handling: Handle errors gracefully with informative messages
- DRY principle: Don't Repeat Yourself - extract common logic into reusable functions
- SOLID principles: Follow object-oriented design principles where applicable
Example Code Style
class ExampleClass:
"""A well-documented example class.
This class demonstrates the coding standards used in `h5forest`.
Attributes:
name (str): The name of the example.
value (int): An example integer value.
"""
def __init__(self, name: str, value: int = 0):
"""Initialize the example class.
Args:
name: The name for this example.
value: Initial value (defaults to 0).
"""
self.name = name
self.value = value
def process_data(self, data: list) -> list:
"""Process the input data.
Args:
data: List of items to process.
Returns:
Processed list with modifications applied.
Raises:
ValueError: If data is empty or invalid.
"""
if not data:
raise ValueError("Data cannot be empty")
return [item * 2 for item in data if isinstance(item, (int, float))]
Testing Requirements
All contributions must include appropriate tests. See the Testing Guide for details.
Test Coverage
- New features: Must include comprehensive tests
- Bug fixes: Must include regression tests
- Refactoring: Existing tests must pass
Running Tests
# Run all tests
pytest
# Run with coverage
pytest --cov=src/h5forest --cov-report=term-missing
# Run specific tests
pytest tests/unit/test_node.py
Documentation
Docstring Style
Use Google-style docstrings:
def example_function(param1: str, param2: int = 10) -> bool:
"""Brief description of the function.
Longer description if needed, explaining the purpose,
behavior, and any important details.
Args:
param1: Description of the first parameter.
param2: Description of the second parameter with default value.
Returns:
Description of the return value.
Raises:
ValueError: Description of when this error is raised.
Example:
>>> example_function("test", 5)
True
"""
Documentation Updates
- Update relevant documentation when changing functionality
- Add new documentation for new features
- Keep examples up to date
Building Documentation Locally
To build and preview the documentation:
# Install documentation dependencies
pip install -e ".[docs]"
# Serve documentation locally with live reload
mkdocs serve
Then visit http://localhost:8000 in your browser to view the documentation.
The documentation will automatically rebuild as you make changes to the markdown files.
Review Process
Pull Request Requirements
Before submitting a PR, ensure:
- [ ] All tests pass (
pytest) - [ ] Code passes linting (
ruff check .) - [ ] Code is properly formatted (
ruff format .) - [ ] New functionality includes tests
- [ ] Documentation is updated if needed
- [ ] Commit messages are descriptive
Review Criteria
PRs are reviewed for:
- Functionality: Does it work as intended?
- Code quality: Is it well-written and maintainable?
- Testing: Are there adequate tests?
- Documentation: Is it properly documented?
- Performance: Does it introduce performance regressions?
- Compatibility: Does it maintain backward compatibility?
Continuous Integration
All PRs automatically run through CI which checks:
- Tests against Python 3.9, 3.10, 3.11, 3.12, 3.13
- Cross-platform testing on Ubuntu, Windows, and macOS
- Code linting and formatting
- Test coverage reporting
- Documentation building (if applicable)
Adding New Key Bindings
The key binding system in h5forest is centralized in the H5KeyBindings class located in src/h5forest/bindings/bindings.py. This section explains how to add new key bindings to the application.
Architecture Overview
The binding system consists of:
H5KeyBindingsclass (src/h5forest/bindings/bindings.py): Central class that manages all key bindings- Function modules (
src/h5forest/bindings/*_funcs.py): Contain the actual handler functions for each mode normal_funcs.py- Mode switching and core operationstree_funcs.py- Tree navigation operationsdataset_funcs.py- Dataset inspection operationshist_funcs.py- Histogram mode operationsplot_funcs.py- Scatter plot mode operationsjump_funcs.py- Jump/goto operationssearch_funcs.py- Search operationswindow_funcs.py- Window focus managementutils.py- Utility functions
Steps to Add a New Binding
1. Create the Handler Function
Add your handler function to the appropriate *_funcs.py file. All handlers should:
- Accept an
eventparameter - Use the
@error_handlerdecorator - Get the app instance via the singleton:
app = H5Forest()
Example:
@error_handler
def my_new_function(event):
"""Brief description of what this function does."""
# Avoid circular imports
from h5forest.h5_forest import H5Forest
# Access the application instance
app = H5Forest()
# Your logic here
app.print("My new function executed!")
2. Import the Handler in bindings.py
Add your function to the imports at the top of src/h5forest/bindings/bindings.py:
3. Add the Key Configuration
In the H5KeyBindings.__init__() method, add a key configuration attribute:
self.my_new_key = self.config.get_keymap(
"your_mode", # e.g., "hist_mode", "plot_mode", "normal_mode"
"your_action", # e.g., "my_action"
)
4. Create a Filter (if needed)
If your binding should only work in certain conditions, add a filter lambda in __init__():
Common filters:
- self.filter_normal_mode - Only in normal mode
- self.filter_hist_mode - Only in histogram mode
- self.filter_tree_focus - Only when tree has focus
- self.filter_not_normal_mode - In any leader mode
5. Create a Label
Add a label for the hotkey display:
6. Bind the Function
In the appropriate _init_*_bindings() method, bind your function:
def _init_your_mode_bindings(self):
"""Initialize your mode keybindings."""
self.bind_function(
self.my_new_key,
my_new_function,
self.filter_your_mode, # Or lambda: True for always active
)
7. Update Hotkey Display
In the get_current_hotkeys() method, add your label to the appropriate mode section:
8. Add Configuration Defaults
Update src/h5forest/data/default_config.yaml to include the default key mapping:
9. Write Tests
Add tests in tests/unit/test_your_mode_bindings.py:
@patch('h5forest.h5_forest.H5Forest')
def test_my_new_function(self, mock_h5forest_class, mock_app, mock_event):
"""Test my new function."""
mock_h5forest_class.return_value = mock_app
_init_your_mode_bindings(mock_app)
# Find the binding
bindings = [b for b in mock_app.kb.bindings if b.keys == ("k",)]
handler = bindings[0].handler
# Call the handler
handler(mock_event)
# Assert expected behavior
mock_app.print.assert_called_once_with("My new function executed!")
Example: Adding a "Copy Value" Binding to Histogram Mode
1. Create handler in hist_funcs.py:
@error_handler
def copy_hist_value(event):
"""Copy the current histogram bin value to clipboard."""
from h5forest.h5_forest import H5Forest
app = H5Forest()
# Get current bin value logic here
value = app.histogram_plotter.get_current_bin_value()
# Copy to clipboard logic
subprocess.run(["xclip", "-selection", "clipboard"],
input=str(value).encode())
app.print(f"Copied value: {value}")
2. Import in bindings.py:
3-7. Add configuration, filter, label, binding, and hotkey display (as shown above)
8. Update default_config.yaml:
9. Write tests (as shown above)
Testing Your Binding
- Run unit tests:
pytest tests/unit/test_your_mode_bindings.py - Test manually: Start
h5forestand try your new key binding - Check pre-commit hooks:
pre-commit run --all-files
Getting Help
Communication Channels
- GitHub Issues: For bugs, feature requests, and questions
- GitHub Discussions: For general questions and community chat
- Documentation: Check the docs first for answers
Thank you for contributing to h5forest! Your efforts help make HDF5 data exploration better for everyone.