Thank you for your interest in contributing to Numerous Widgets! This document provides comprehensive guidelines and instructions for contributing to this project.
- Python 3.11+
- Node.js 18+
- npm
- Git
-
Fork and clone the repository:
git clone https://github.com/your-username/numerous-widgets.git cd numerous-widgets -
Set up Python environment:
python -m venv .venv source .venv/bin/activate # On Windows: .venv\Scripts\activate pip install -e ".[dev]"
-
Set up JavaScript environment:
cd js npm install cd ..
-
Install pre-commit hooks:
pre-commit install
-
Create environment configuration:
# Create .env file in python/ directory echo "WIDGET_ENV=development" > python/.env echo "VITE_DEV_SERVER=http://localhost:5173" >> python/.env
We maintain high code quality standards to ensure reliability and maintainability. All contributions must meet these requirements before being merged. We're transparent about what's expected - check the configuration files for exact specifications.
Before contributing, ensure your code meets these standards:
Python Requirements:
- ✅ Linting: Code passes Ruff checks
- ✅ Formatting: Code is formatted with Ruff
- ✅ Type Safety: All code has proper type annotations and passes MyPy strict mode
- ✅ Testing: All new code has comprehensive tests with good coverage
- ✅ Documentation: All public APIs are documented
JavaScript/TypeScript Requirements:
- ✅ Type Safety: All TypeScript compiles without errors
- ✅ Testing: All components have unit tests
- ✅ Build: Widgets build successfully
Configuration Files (see exact requirements here):
- 📁
.pre-commit-config.yaml- Pre-commit hooks and quality checks - 📁
pyproject.toml- Python project configuration, Ruff rules, MyPy settings - 📁
js/package.json- JavaScript dependencies and scripts - 📁
js/tsconfig.json- TypeScript configuration - 📁
.github/workflows/release.yml- CI/CD pipeline requirements
Quick checks (run automatically on commit/push):
# These run automatically via pre-commit hooks
git commit -m "your changes" # Triggers basic formatting and linting
git push # Triggers basic testsManual quality checks:
# Run all basic checks
pre-commit run --all-files
# Run specific checks
cd js
npm run lint # ESLint
npm run typecheck # TypeScript
npm test # Jest testsCI simulation (run before creating PR):
# Simulate full CI pipeline locally - this is what runs on GitHub Actions
pre-commit run --hook-stage manual
# Or run individual CI checks:
pre-commit run --hook-stage manual ruff-check-strict # Strict linting
pre-commit run --hook-stage manual mypy-strict # Strict type checking
pre-commit run --hook-stage manual ci-check-python # Python tests with coverage
pre-commit run --hook-stage manual ci-check-javascript # JavaScript tests
pre-commit run --hook-stage manual ci-check-build-widgets # Build verificationWe follow these principles:
- Transparency: All requirements are clearly documented in config files
- Developer Choice: You control when to run strict checks
- Fast Feedback: Basic checks run quickly on commit/push
- CI Confidence: Local CI simulation helps avoid pipeline failures
- Comprehensive Coverage: All code paths are tested
💡 Pro Tip: Run pre-commit run --hook-stage manual before creating a PR to ensure CI will pass!
The project uses a dual-environment setup:
- Development Mode: JavaScript served from Vite dev server
- Production Mode: JavaScript served from built static files
Terminal 1 - Vite Development Server:
cd js/src
npx viteTerminal 2 - Marimo Development:
cd python/examples/marimo/numerous
marimo edit app.pyThis setup enables hot-reloading for both Python and JavaScript changes.
The project enforces strict code quality standards. See the Quality Requirements & Standards section above for complete details.
Quick reference:
- Python: Ruff (linting/formatting), MyPy (type checking), pytest (testing)
- JavaScript/TypeScript: ESLint, TypeScript compiler, Jest (testing)
- All quality checks: See configuration files linked above for exact specifications
Each widget consists of three main components:
- Python Widget Class (
python/src/numerous/widgets/base/) - React Widget Component (
js/src/components/widgets/) - React UI Component (
js/src/components/ui/)
Let's create a new widget called ExampleWidget:
Create python/src/numerous/widgets/base/example.py:
"""Module providing an example widget for the numerous library."""
import anywidget
import traitlets
from .config import get_widget_paths
# Get environment-appropriate paths
ESM, CSS = get_widget_paths("ExampleWidget")
class Example(anywidget.AnyWidget): # type: ignore[misc]
"""
An example widget for demonstration purposes.
Args:
value: The initial value
label: The widget label
on_change: Optional callback when value changes
"""
# Define traitlets for the widget properties
value = traitlets.Unicode().tag(sync=True)
label = traitlets.Unicode().tag(sync=True)
disabled = traitlets.Bool(default_value=False).tag(sync=True)
# Load the JavaScript and CSS
_esm = ESM
_css = CSS
def __init__(
self,
value: str = "",
label: str = "",
disabled: bool = False,
) -> None:
super().__init__(
value=value,
label=label,
disabled=disabled,
)
@property
def val(self) -> str:
"""Return the current value."""
return str(self.value)
@val.setter
def val(self, value: str) -> None:
"""Set the current value."""
self.value = valueCreate js/src/components/ui/Example.tsx:
import * as React from "react";
interface ExampleProps {
value: string;
label: string;
disabled?: boolean;
onChange: (value: string) => void;
}
export function Example({
value,
label,
disabled = false,
onChange
}: ExampleProps) {
return (
<div className="example-container">
<label className="example-label">{label}</label>
<input
type="text"
value={value}
disabled={disabled}
onChange={(e) => onChange(e.target.value)}
className="example-input"
/>
</div>
);
}Create js/src/components/widgets/ExampleWidget.tsx:
import * as React from "react";
import { createRender, useModelState } from "@anywidget/react";
import { Example } from "../ui/Example";
import '../../css/styles.scss';
function ExampleWidget() {
// Model states
const [value, setValue] = useModelState<string>("value");
const [label] = useModelState<string>("label");
const [disabled] = useModelState<boolean>("disabled");
return (
<Example
value={value}
label={label}
disabled={disabled}
onChange={setValue}
/>
);
}
export default {
render: createRender(ExampleWidget)
};Add to js/widget-config.json:
{
"widgets": [
"ExampleWidget",
// ... other widgets
]
}Add to python/src/numerous/widgets/__init__.py:
from .base.example import Example as ExampleAdd to docs/widgets.md:
## ::: widgets.Example
options:
show_root_heading: true# Build the widget
cd js
./build-widgets.sh # On Linux/Mac
# or
./build-widgets.ps1 # On Windows
# Test the widget
cd ..
python -c "
import numerous.widgets as wi
widget = wi.Example(value='Hello', label='Test')
print(f'Widget created: {widget.val}')
"IMPORTANT: Every new widget must include comprehensive tests for both Python and JavaScript components.
Create python/tests/test_example.py:
import pytest
from numerous.widgets import Example
def test_example_creation():
"""Test basic widget creation with default values."""
widget = Example(value="test", label="Test Label")
assert widget.value == "test"
assert widget.label == "Test Label"
assert widget.disabled is False
def test_example_with_custom_options():
"""Test widget creation with all custom options."""
widget = Example(
value="custom value",
label="Custom Label",
disabled=True,
class_name="custom-class"
)
assert widget.value == "custom value"
assert widget.label == "Custom Label"
assert widget.disabled is True
assert widget.class_name == "custom-class"
def test_example_val_property():
"""Test the val property getter and setter."""
widget = Example(value="initial")
assert widget.val == "initial"
widget.val = "updated"
assert widget.value == "updated"
assert widget.val == "updated"
def test_example_callback():
"""Test callback functionality if widget has callbacks."""
callback_calls = []
def on_change_callback(change):
callback_calls.append(change)
widget = Example(
value="test",
on_change=on_change_callback
)
# Clear initial calls (initialization triggers observers)
callback_calls.clear()
# Simulate state change
widget.some_state = "new_value"
assert len(callback_calls) == 1
assert callback_calls[0]["new"] == "new_value"
def test_example_edge_cases():
"""Test edge cases like empty values, unicode, etc."""
# Empty value
widget = Example(value="")
assert widget.value == ""
# Unicode characters
unicode_text = "Hello 🌍! Unicode: αβγ"
widget = Example(value=unicode_text)
assert widget.value == unicode_textCreate Widget Component Test: js/src/components/widgets/__tests__/ExampleWidget.test.tsx:
import React from 'react';
import ExampleWidget from '../ExampleWidget';
import { Example } from '../../ui/Example';
import * as ExampleWidgetModule from '../ExampleWidget';
// Mock dependencies
jest.mock('../../ui/Example', () => ({
Example: jest.fn(() => null)
}));
jest.mock('../../css/styles.scss', () => ({}));
jest.mock('@anywidget/react', () => ({
createRender: jest.fn(comp => comp),
useModelState: jest.fn()
}));
const mockSetValue = jest.fn();
describe('ExampleWidget', () => {
beforeEach(() => {
jest.clearAllMocks();
(jest.requireMock('@anywidget/react').useModelState)
.mockImplementation((key: string) => {
switch (key) {
case 'value': return ['test value', mockSetValue];
case 'label': return ['Test Label'];
case 'disabled': return [false];
default: return [undefined, jest.fn()];
}
});
});
it('renders Example component with correct props', () => {
const useCallbackSpy = jest.spyOn(React, 'useCallback');
useCallbackSpy.mockImplementation(fn => fn);
const ExampleWidgetFunction = (ExampleWidgetModule as any).default.render;
const exampleWidget = ExampleWidgetFunction();
expect(exampleWidget.type).toBe(Example);
expect(exampleWidget.props.value).toBe('test value');
expect(exampleWidget.props.label).toBe('Test Label');
expect(exampleWidget.props.disabled).toBe(false);
expect(typeof exampleWidget.props.onChange).toBe('function');
useCallbackSpy.mockRestore();
});
it('handles value changes correctly', () => {
const useCallbackSpy = jest.spyOn(React, 'useCallback');
useCallbackSpy.mockImplementation(fn => fn);
const ExampleWidgetFunction = (ExampleWidgetModule as any).default.render;
const exampleWidget = ExampleWidgetFunction();
exampleWidget.props.onChange('new value');
expect(mockSetValue).toHaveBeenCalledWith('new value');
useCallbackSpy.mockRestore();
});
});Create UI Component Test: js/src/components/ui/__tests__/Example.test.tsx:
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { Example } from '../Example';
describe('Example UI Component', () => {
const defaultProps = {
value: 'test value',
label: 'Test Label',
disabled: false,
onChange: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
});
it('renders correctly with default props', () => {
render(<Example {...defaultProps} />);
expect(screen.getByText('Test Label')).toBeInTheDocument();
expect(screen.getByDisplayValue('test value')).toBeInTheDocument();
});
it('handles user interactions', () => {
render(<Example {...defaultProps} />);
const input = screen.getByDisplayValue('test value');
fireEvent.change(input, { target: { value: 'new value' } });
expect(defaultProps.onChange).toHaveBeenCalledWith('new value');
});
it('respects disabled state', () => {
render(<Example {...defaultProps} disabled={true} />);
const input = screen.getByDisplayValue('test value');
expect(input).toBeDisabled();
});
it('applies custom className', () => {
render(<Example {...defaultProps} className="custom-class" />);
const container = screen.getByText('Test Label').parentElement;
expect(container).toHaveClass('custom-class');
});
});-
Python Test Coverage:
- Basic widget creation with default values
- Widget creation with all custom options
- Property getters and setters (especially
valproperty) - Callback functionality (if applicable)
- Edge cases: empty values, unicode, special characters
- Error handling scenarios
-
JavaScript Test Coverage:
- Widget component renders UI component correctly
- All props are passed through correctly
- State changes trigger correct Python model updates
- User interactions work as expected
- UI component respects all props (disabled, className, etc.)
- Visual states and feedback work correctly
-
Integration Tests:
- Widget can be imported and instantiated
- Widget displays in development environment
- Callbacks trigger correctly when user interacts
- State synchronization between Python and JavaScript
# Run Python tests for specific widget
pytest python/tests/test_example.py -v
# Run JavaScript tests for specific widget
cd js
npm test -- Example
# Run all tests
pre-commit run --all-files- Python:
python/tests/test_widget_name.py - Widget Component:
js/src/components/widgets/__tests__/WidgetNameWidget.test.tsx - UI Component:
js/src/components/ui/__tests__/ComponentName.test.tsx
We use a three-tier testing approach:
- Basic Tests (run on pre-push): Fast, essential functionality
- Comprehensive Tests (run manually): Full test suite with coverage
- CI Tests (run on GitHub Actions): Production-ready validation
Basic testing (fast, run automatically):
# Run basic tests (triggered on git push)
git push
# Or run manually
pre-commit run pytest-basic jest-basicComprehensive testing (full coverage):
# Run all tests with coverage (CI simulation)
pre-commit run --hook-stage manual ci-check-python ci-check-javascript
# Or run individual test suites
pytest python/tests/ -v # Python tests
cd js && npm test # JavaScript tests
python -m coverage run -m pytest python/tests/ # Python with coverageCI simulation (exact match to GitHub Actions):
# Run complete CI pipeline locally
pre-commit run --hook-stage manual
# This includes:
# - Strict linting (ruff --no-fix)
# - Strict type checking (mypy --strict)
# - Python tests with coverage reporting
# - JavaScript tests with npm ci
# - Widget build verification- Python: Minimum 80% test coverage (measured in CI)
- JavaScript: All components should have unit tests
- Integration: Test widget creation and basic functionality
- Type Safety: All TypeScript must compile without errors
- Build: Widgets must build successfully without errors
Follow these naming conventions:
- Python:
python/tests/test_widget_name.py - Widget Component:
js/src/components/widgets/__tests__/WidgetNameWidget.test.tsx - UI Component:
js/src/components/ui/__tests__/ComponentName.test.tsx
-
Run CI simulation (recommended):
# This runs the same checks as GitHub Actions pre-commit run --hook-stage manual -
Alternative: Run individual checks:
# Basic quality checks pre-commit run --all-files # Test Python widgets pytest python/tests/ # Test JavaScript components cd js && npm test # Build widgets cd js && bash build-widgets.sh
-
Test in development environment:
# Start Vite dev server cd js && npm run dev # Test in Marimo cd python/examples/marimo/numerous marimo edit app.py
Before creating your PR, ensure:
- ✅ CI simulation passes:
pre-commit run --hook-stage manual - ✅ All tests pass: Both Python and JavaScript
- ✅ Widgets build successfully: No build errors
- ✅ Code is documented: New widgets have proper docstrings
- ✅ Tests are comprehensive: New functionality is tested
- ✅ Examples work: Test your changes in the development environment
-
Title: Use conventional commits format
feat: add new Example widgetfix: resolve button click issuedocs: update contributing guidelines
-
Description: Include:
- Summary of changes
- Screenshots/videos for UI changes
- Breaking changes (if any)
- Testing instructions
-
Checklist:
- All tests pass
- Code follows style guidelines
- Documentation updated
- Backward compatibility maintained
- Performance impact considered
The project uses Semantic Versioning:
- MAJOR: Breaking changes
- MINOR: New features (backward compatible)
- PATCH: Bug fixes (backward compatible)
- Development: Work on feature branches
- Pull Request: Submit PR to main branch
- Review: Code review and approval
- Merge: Merge to main triggers automated release
- Release: GitHub Actions automatically:
- Builds all widgets
- Runs comprehensive tests
- Determines version bump
- Creates GitHub release
- Publishes to PyPI
# Update version and create release
pip install python-semantic-release
semantic-release version
semantic-release publishAll widgets must include:
- Docstrings: Complete parameter documentation
- Examples: Usage examples in docstrings
- Type Hints: Full type annotations
- API Reference: Auto-generated from docstrings
# Build documentation locally
mkdocs serve
# Generate API reference
cd scripts
python gen_ref_pages.pyInclude:
- Environment: Python version, browser, OS
- Steps to reproduce: Minimal example
- Expected behavior: What should happen
- Actual behavior: What actually happens
- Error messages: Full stack traces
Include:
- Use case: Why is this needed?
- Proposal: How should it work?
- Examples: Mock code or wireframes
- Alternatives: Other solutions considered
- GitHub Issues: Bug reports and feature requests
- GitHub Discussions: General questions and ideas
- Email: support@numerous.com
Please be respectful and inclusive. We welcome contributors from all backgrounds and experience levels.
- AnyWidget Documentation: https://anywidget.dev/
- React Documentation: https://react.dev/
- TypeScript Documentation: https://www.typescriptlang.org/
- Vite Documentation: https://vitejs.dev/
- Marimo Documentation: https://marimo.io/
Thank you for contributing to Numerous Widgets! 🎉