Creating Custom Modules¶
This guide walks you through creating a FuncNodes module from scratch — a Python package that provides nodes for the FuncNodes ecosystem.
Overview¶
A FuncNodes module is a standard Python package with:
- Node definitions — Functions or classes decorated as nodes
- Shelf organization — Grouping nodes into a browsable hierarchy
- Entry points — Registration so FuncNodes discovers your module
- Optional: React plugin — Custom UI components for your nodes
my_funcnodes_module/
├── pyproject.toml # Package metadata + entry points
├── src/
│ └── funcnodes_mymodule/
│ ├── __init__.py # Exports NODE_SHELF
│ ├── nodes.py # Node definitions
│ └── _react_plugin.py # Optional: React plugin info
└── react_plugin/ # Optional: React UI components
├── package.json
├── vite.config.ts
└── src/
└── index.tsx
Quick Start with funcnodes_module¶
The fastest way to create a module is using the funcnodes_module scaffolding tool:
# Install the tool
pip install funcnodes-module
# Create a new module
funcnodes_module create funcnodes_mymodule
# Or with options
funcnodes_module create funcnodes_mymodule \
--description "My awesome nodes" \
--author "Your Name" \
--with-react-plugin
This generates a complete project structure with:
- Pre-configured pyproject.toml
- Example node definitions
- Test setup with funcnodes_pytest
- Optional React plugin scaffold
Manual Setup¶
Step 1: Project Structure¶
Create your package structure:
mkdir funcnodes_mymodule
cd funcnodes_mymodule
mkdir -p src/funcnodes_mymodule
touch src/funcnodes_mymodule/__init__.py
touch src/funcnodes_mymodule/nodes.py
touch pyproject.toml
Step 2: pyproject.toml¶
Configure your package with the required entry points:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "funcnodes-mymodule"
version = "0.1.0"
description = "My custom FuncNodes module"
readme = "README.md"
license = "MIT"
authors = [
{ name = "Your Name", email = "you@example.com" }
]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
]
dependencies = [
"funcnodes-core>=0.2.0",
]
[project.optional-dependencies]
dev = [
"pytest",
"funcnodes-pytest",
]
# CRITICAL: Entry points for FuncNodes discovery
[project.entry-points."funcnodes.module"]
module = "funcnodes_mymodule"
shelf = "funcnodes_mymodule:NODE_SHELF"
[tool.hatch.build.targets.wheel]
packages = ["src/funcnodes_mymodule"]
Step 3: Define Nodes¶
Create your nodes in src/funcnodes_mymodule/nodes.py:
import funcnodes_core as fn
from typing import List
@fn.NodeDecorator(
node_id="funcnodes_mymodule.greet",
name="Greet",
description="Creates a greeting message"
)
def greet(name: str = "World") -> str:
"""Generate a greeting for the given name."""
return f"Hello, {name}!"
@fn.NodeDecorator(
node_id="funcnodes_mymodule.sum_list",
name="Sum List",
description="Sums all numbers in a list"
)
def sum_list(numbers: List[float]) -> float:
"""Calculate the sum of a list of numbers."""
return sum(numbers)
@fn.NodeDecorator(
node_id="funcnodes_mymodule.multiply",
name="Multiply",
description="Multiplies two numbers"
)
def multiply(
a: float = 1.0,
b: float = 1.0
) -> float:
"""Multiply two numbers together."""
return a * b
Step 4: Create the Shelf¶
Export your nodes in src/funcnodes_mymodule/__init__.py:
from funcnodes_core import Shelf
from .nodes import greet, sum_list, multiply
# Define the shelf structure
NODE_SHELF = Shelf(
name="My Module",
description="Custom nodes for demonstration",
nodes=[greet, sum_list, multiply],
subshelves=[]
)
# Export for convenience
__all__ = ["NODE_SHELF", "greet", "sum_list", "multiply"]
Step 5: Install and Test¶
# Install in development mode
pip install -e .
# Verify discovery
funcnodes modules list
# Should show: funcnodes_mymodule
# Test in the UI
funcnodes runserver
Entry Points Reference¶
The [project.entry-points."funcnodes.module"] section tells FuncNodes how to load your module:
| Entry Point | Required | Description |
|---|---|---|
module |
Yes | Import path to your module root |
shelf |
Recommended | Path to your NODE_SHELF object |
react_plugin |
Optional | Path to React plugin info dict |
render_options |
Optional | Custom render options for types |
external_worker |
Optional | Custom worker class |
Example with All Entry Points¶
[project.entry-points."funcnodes.module"]
module = "funcnodes_mymodule"
shelf = "funcnodes_mymodule:NODE_SHELF"
react_plugin = "funcnodes_mymodule:REACT_PLUGIN"
render_options = "funcnodes_mymodule:RENDER_OPTIONS"
Shelf Organization¶
Flat Structure¶
For simple modules with few nodes:
Nested Structure¶
For larger modules, use subshelves:
from funcnodes_core import Shelf
from .math_nodes import add, subtract, multiply, divide
from .string_nodes import concat, split, upper, lower
from .io_nodes import read_file, write_file
NODE_SHELF = Shelf(
name="My Module",
description="Comprehensive utility nodes",
subshelves=[
Shelf(
name="Math",
description="Mathematical operations",
nodes=[add, subtract, multiply, divide]
),
Shelf(
name="Strings",
description="String manipulation",
nodes=[concat, split, upper, lower]
),
Shelf(
name="I/O",
description="File operations",
nodes=[read_file, write_file]
),
]
)
Best Practices¶
- Consistent naming: Use lowercase with underscores for node_id
- Prefix with module name:
funcnodes_mymodule.category.node_name - Group by function: Math, I/O, Transforms, etc.
- Limit depth: 2-3 levels of subshelves maximum
- Add descriptions: Help users understand each shelf's purpose
Advanced Node Patterns¶
Nodes with Multiple Outputs¶
from typing import Tuple
@fn.NodeDecorator(
node_id="funcnodes_mymodule.divmod",
name="Divmod",
outputs=[
{"name": "quotient"},
{"name": "remainder"}
]
)
def divmod_node(a: int, b: int) -> Tuple[int, int]:
"""Return quotient and remainder of division."""
return divmod(a, b)
Nodes with Dynamic Options¶
from funcnodes_core.io_hooks import update_other_io_options
@fn.NodeDecorator(node_id="funcnodes_mymodule.select_column")
@update_other_io_options("column", modifier=lambda df: df.columns.tolist())
def select_column(df: "pd.DataFrame", column: str) -> "pd.Series":
"""Select a column from a DataFrame."""
return df[column]
Nodes with Value Constraints¶
@fn.NodeDecorator(
node_id="funcnodes_mymodule.clamp",
name="Clamp Value"
)
def clamp(
value: float,
min_val: float = 0.0,
max_val: float = 1.0
) -> float:
"""Clamp a value to a range."""
return max(min_val, min(max_val, value))
# Add constraints via class-based approach for more control
class ClampNode(fn.Node):
node_id = "funcnodes_mymodule.clamp_v2"
node_name = "Clamp (Advanced)"
value = fn.NodeInput(id="value", type=float, default=0.5)
min_val = fn.NodeInput(
id="min_val",
type=float,
default=0.0,
value_options={"max": 1.0}
)
max_val = fn.NodeInput(
id="max_val",
type=float,
default=1.0,
value_options={"min": 0.0}
)
result = fn.NodeOutput(id="result", type=float)
async def func(self, value, min_val, max_val):
self.outputs["result"].value = max(min_val, min(max_val, value))
Async Nodes¶
import aiohttp
@fn.NodeDecorator(node_id="funcnodes_mymodule.fetch_url")
async def fetch_url(url: str) -> str:
"""Fetch content from a URL."""
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
Heavy Computation Nodes¶
@fn.NodeDecorator(
node_id="funcnodes_mymodule.heavy_compute",
separate_thread=True # Run in thread pool
)
def heavy_compute(data: list) -> list:
"""CPU-intensive operation."""
import time
time.sleep(2) # Simulating heavy work
return [x * 2 for x in data]
@fn.NodeDecorator(
node_id="funcnodes_mymodule.very_heavy",
separate_process=True # Run in separate process
)
def very_heavy_compute(data: list) -> list:
"""Very CPU-intensive operation (process isolation)."""
# This runs in a completely separate process
return expensive_operation(data)
Nodes with Progress¶
@fn.NodeDecorator(node_id="funcnodes_mymodule.process_items")
async def process_items(items: list, node: fn.Node = None) -> list:
"""Process items with progress reporting."""
results = []
for item in node.progress(items):
results.append(await process_single(item))
return results
Custom Types and Rendering¶
DataEnum for Dropdowns¶
from funcnodes_core import DataEnum
class ColorMode(DataEnum):
RGB = ("rgb", "RGB Color")
HSV = ("hsv", "HSV Color")
GRAYSCALE = ("gray", "Grayscale")
@fn.NodeDecorator(node_id="funcnodes_mymodule.convert_color")
def convert_color(image: "np.ndarray", mode: ColorMode = ColorMode.RGB) -> "np.ndarray":
"""Convert image color mode."""
return do_conversion(image, mode.v())
Custom Render Options¶
Register custom render options for your types:
# In __init__.py
RENDER_OPTIONS = {
"typemap": {
"MyCustomType": "custom_renderer"
},
"inputconverter": {
"MyCustomType": "custom_input"
}
}
Preview Rendering¶
Configure how node outputs are previewed:
class ImageProcessorNode(fn.Node):
node_id = "funcnodes_mymodule.image_processor"
# Tell UI to render the 'output_image' as the preview
default_render_options = {
"data": {"src": "output_image"}
}
input_image = fn.NodeInput(id="input_image", type="np.ndarray")
output_image = fn.NodeOutput(id="output_image", type="np.ndarray")
Publishing Your Module¶
1. Prepare for Release¶
2. Build the Package¶
3. Publish to PyPI¶
4. Register with funcnodes_repositories (Optional)¶
To appear in the official module list:
- Fork funcnodes_repositories
- Add your module to
available_repositories.yaml:
- Submit a pull request
Testing Your Module¶
Setup pytest-funcnodes¶
Write Node Tests¶
# tests/test_nodes.py
import pytest
from funcnodes_pytest import nodetest, all_nodes_tested
from funcnodes_mymodule import NODE_SHELF
from funcnodes_mymodule.nodes import greet, sum_list, multiply
@nodetest
async def test_greet():
node = greet()
node.inputs["name"].value = "Alice"
await node
assert node.outputs["out"].value == "Hello, Alice!"
@nodetest
async def test_sum_list():
node = sum_list()
node.inputs["numbers"].value = [1, 2, 3, 4, 5]
await node
assert node.outputs["out"].value == 15
@nodetest
async def test_multiply():
node = multiply()
node.inputs["a"].value = 3.0
node.inputs["b"].value = 4.0
await node
assert node.outputs["out"].value == 12.0
def test_all_nodes_covered(all_nodes):
"""Ensure all nodes in the shelf have tests."""
all_nodes_tested(all_nodes, NODE_SHELF)
Run Tests¶
# Run all tests
pytest
# Run only node tests
pytest --nodetests-only
# With coverage
pytest --cov=funcnodes_mymodule
See Testing Modules for comprehensive testing guidance.
React Plugin Development¶
For custom UI components (editors, previews, widgets), see React Plugins.
Quick overview:
// react_plugin/src/index.tsx
import { FuncNodesReactPlugin } from "@linkdlab/funcnodes_react_flow";
const MyPlugin: FuncNodesReactPlugin = {
renderNodeOutput: (props) => {
if (props.type === "MyCustomType") {
return <MyCustomPreview data={props.value} />;
}
return null;
}
};
export default MyPlugin;
Common Patterns¶
File-Based Nodes¶
from pathlib import Path
@fn.NodeDecorator(node_id="funcnodes_mymodule.read_json")
def read_json(filepath: str) -> dict:
"""Read a JSON file."""
import json
with open(filepath) as f:
return json.load(f)
Configuration Nodes¶
from dataclasses import dataclass
@dataclass
class Config:
threshold: float = 0.5
max_iterations: int = 100
@fn.NodeDecorator(node_id="funcnodes_mymodule.create_config")
def create_config(
threshold: float = 0.5,
max_iterations: int = 100
) -> Config:
"""Create a configuration object."""
return Config(threshold=threshold, max_iterations=max_iterations)
Wrapper Nodes for External Libraries¶
@fn.NodeDecorator(
node_id="funcnodes_mymodule.sklearn_fit",
name="Fit Model"
)
def fit_model(
model: "sklearn.base.BaseEstimator",
X: "np.ndarray",
y: "np.ndarray"
) -> "sklearn.base.BaseEstimator":
"""Fit a scikit-learn model."""
return model.fit(X, y)
Checklist¶
Before publishing, ensure:
- [ ] All nodes have unique
node_idwith module prefix - [ ] All nodes have
nameanddescription - [ ] All inputs have type hints
- [ ] All outputs have type hints
- [ ] Shelf is properly organized
- [ ] Entry points are configured in
pyproject.toml - [ ] Tests cover all nodes
- [ ] README documents installation and usage
- [ ] Version follows semantic versioning
See Also¶
- React Plugins — Custom UI components
- Testing Modules — Comprehensive testing guide
- Creating Nodes — Node fundamentals
- Shelves — Shelf organization
- Available Modules — Official module examples