Visual, story-based component development for Python 3.14+
Storyville is a visual, component-driven development (CDD) system for Python that helps you build, document, and test components in isolation. Write stories to express component variations, browse them in a live catalog, and automatically generate tests from assertions.
💡 Think Storybook.js for Python — but with native Python 3.14+ features, hot reload via subinterpreters, and automatic pytest integration!
- 🎨 Building component libraries with tdom
- 👀 Visual component development and documentation
- ✅ Test-driven component design
- 🔥 Hot-reloading Python modules during development
|
|
|
|
|
|
# Requires Python 3.14+
pip install storyville
⚠️ Note: Storyville requires Python 3.14+ for subinterpreter support and modern type syntax.
Want to play around? Use the seed command to generate a fake catalog, then view it.
$ cd [some temp dir]
$ uvx storyville seed small my_catalog
$ PYTHONPATH=. uvx storyville serve my_catalogThen open http://localhost:8080 in your browser. Edit files in my_catalog and see the updates.
# my_package/components/button/button.py
from tdom import html
def Button(text: str, variant: str = "primary"):
"""A simple button component."""
return html(t"<button class={variant}>{text}</button>")# my_package/components/button/stories.py
from my_package.components.button.button import Button
from storyville import Story, Subject
from storyville.assertions import GetByTagName
def this_subject() -> Subject:
return Subject(
title="Button Component",
target=Button,
items=[
# Story with assertions
Story(
props=dict(text="Click Me", variant="primary"),
assertions=[GetByTagName(tag="button")],
),
# More variations...
Story(props=dict(text="Cancel", variant="danger")),
],
)New to Storyville? Generate a complete example catalog to learn from:
# Generate a small example catalog
storyville seed small my_catalog
# Or try medium or large catalogs
storyville seed medium my_catalog
storyville seed large my_catalogCatalog Sizes:
- small: 1 section, 2-3 subjects, 2 stories per subject (4-6 total stories)
- medium: 2-3 sections, 4-6 subjects, 2-3 stories per subject (12-18 total stories)
- large: 4-5 sections, 8-12 subjects, 3-4 stories per subject (30-40 total stories)
The generated catalog is a complete Python package with:
- Diverse component examples (Button, Card, Form, Badge, etc.)
- Story assertions demonstrating testing patterns
- Custom ThemedLayout showing layout customization
- Ready to serve and build immediately
storyville serve my_package
# Opens http://localhost:8080
# Hot reload enabled by default!For generated catalogs:
storyville serve my_catalog
# Browse the example components and storiesstoryville build my_catalog dist/
# Generates static HTML in dist/ directory# my_package/themed_layout/themed_layout.py
from dataclasses import dataclass
from tdom import Node, html
@dataclass
class ThemedLayout:
story_title: str | None
children: Node | None
def __call__(self) -> Node:
"""Render the themed layout using tdom t-string."""
title_text = self.story_title if self.story_title else "Story"
return html(t'''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title_text}</title>
<style>
body {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: system-ui;
margin: 0;
padding: 20px;
}}
.story-wrapper {{
max-width: 900px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.95);
padding: 40px;
border-radius: 12px;
}}
</style>
</head>
<body>
<div class="story-wrapper">
{self.children}
</div>
</body>
</html>
''')
# my_package/stories.py
from tdom import Node
from storyville import Catalog
from my_package.themed_layout.themed_layout import ThemedLayout
def themed_layout_wrapper(story_title: str | None = None, children: Node | None = None) -> Node:
"""Wrapper function to create and call ThemedLayout instances."""
layout = ThemedLayout(story_title=story_title, children=children)
return layout()
def this_catalog() -> Catalog:
return Catalog(themed_layout=themed_layout_wrapper)# Configure pytest in pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests", "my_package"]
[tool.storyville.pytest]
enabled = true
# Run tests
pytest my_package/
# Auto-generates tests from story assertions!Storyville provides declarative assertion helpers that wrap aria-testing queries for clean, reusable component assertions:
# my_package/components/button/stories.py
from my_package.components.button.button import Button
from storyville import Story, Subject
from storyville.assertions import GetByTagName, GetByText, GetByRole
def this_subject() -> Subject:
return Subject(
title="Button Component",
target=Button,
items=[
Story(
title="Primary Button",
props=dict(text="Click Me", variant="primary"),
assertions=[
# Element exists
GetByTagName(tag_name="button"),
# Text content verification
GetByTagName(tag_name="button").text_content("Click Me"),
# Attribute checks
GetByTagName(tag_name="button").with_attribute("class", "primary"),
],
),
Story(
title="No Login Button",
props=dict(text="Submit", variant="primary"),
assertions=[
# Negative assertion - element should NOT exist
GetByText(text="Login").not_(),
],
),
],
)Single Element Query Helpers:
GetByRole(role="button")- Find by ARIA roleGetByText(text="Submit")- Find by text contentGetByLabelText(label="Email")- Find form inputs by labelGetByTestId(test_id="submit-btn")- Find by test IDGetByClass(class_name="btn-primary")- Find by CSS classGetById(id="main-content")- Find by element IDGetByTagName(tag_name="button")- Find by HTML tag
Fluent API Modifiers:
.not_()- Assert element does NOT exist.text_content(expected)- Verify element text.with_attribute(name, value)- Check element attribute
Method Chaining:
# Chain multiple checks
GetByTagName(tag_name="button")
.text_content("Save")
.with_attribute("type", "submit")List-Oriented Query Helpers (GetAllBy):*
For assertions involving multiple elements, use the GetAllBy* helpers:
from storyville.assertions import GetAllByRole, GetAllByText, GetAllByClass
Story(
title="Navigation Menu",
props=dict(items=["Home", "About", "Contact"]),
assertions=[
# Verify count of elements
GetAllByRole(role="listitem").count(3),
# Select specific element and verify its properties
GetAllByRole(role="listitem").nth(0).text_content("Home"),
GetAllByRole(role="listitem").nth(1).text_content("About"),
# Chain count with other operations
GetAllByClass(class_name="nav-item").count(3),
# Select element and check its attributes
GetAllByText(text="Contact").nth(0).with_attribute("href", "/contact"),
],
)Available List Query Helpers:
GetAllByRole(role="button")- Find all elements with ARIA roleGetAllByText(text="Item")- Find all elements with textGetAllByLabelText(label="Option")- Find all labeled elementsGetAllByTestId(test_id="card")- Find all elements by test IDGetAllByClass(class_name="item")- Find all elements with CSS classGetAllByTagName(tag_name="li")- Find all elements with HTML tag
List Query Operations:
.count(expected)- Assert exact number of elements found.nth(index)- Select nth element (0-indexed) for further checks- After
.nth(), you can chain.text_content()and.with_attribute()
Example with Complete Workflow:
from storyville.assertions import GetAllByTagName, GetAllByClass
Story(
title="Product List",
props=dict(products=[...]),
assertions=[
# Assert we have exactly 5 products
GetAllByClass(class_name="product-card").count(5),
# Verify first product details
GetAllByClass(class_name="product-card")
.nth(0)
.text_content("Product 1"),
# Verify all buttons are present
GetAllByTagName(tag_name="button").count(5),
# Check specific button attributes
GetAllByTagName(tag_name="button")
.nth(2)
.with_attribute("type", "button"),
],
)All helpers are frozen dataclasses ensuring immutability and type safety. They integrate seamlessly with Story.assertions and pytest test generation.
- Getting Started - Installation and first steps
- Writing Stories - Component stories and assertions
- Themed Stories - Custom layouts and design system integration
- pytest Plugin - Automatic test generation
- Hot Reload - Subinterpreter architecture
- CLI Reference - Command-line interface documentation
- API Reference - Complete API documentation
- ✨ Type statement for type aliases:
type AssertionCallable = Callable[[Element | Fragment], None] - 🔀 PEP 604 union syntax:
X | Yinstead ofUnion[X, Y] - 🔍 Structural pattern matching for clean conditionals
- 🔄 Subinterpreter pool for true module isolation
| Technology | Purpose |
|---|---|
| 🎯 tdom | Templating and HTML generation |
| 🚀 Starlette | Async web framework |
| ✅ pytest | Testing infrastructure |
| 💅 PicoCSS | Semantic CSS framework |
| 👀 watchfiles | Fast file change detection |
📖 Catalog
├─ 📁 Section (optional grouping)
│ └─ 🎯 Subject (component)
│ ├─ 📄 Story (variation)
│ └─ 📄 Story (with ✅ assertions)
└─ 🎯 Subject
└─ 📄 Story
| Use Case | Description |
|---|---|
| 📚 Component Libraries | Build and document reusable components with all their variations in one place |
| 🎨 Design Systems | Create a browseable catalog of your design system components with live examples |
| 🧪 Test-Driven Development | Write assertions alongside stories for immediate visual and automated testing feedback |
| 📖 Living Documentation | Stories serve as both visual documentation and executable examples |
Contributions are welcome! 🎉 This project uses modern Python tooling:
| Tool | Purpose |
|---|---|
| 📦 uv | Dependency management |
| 🧹 ruff | Linting and formatting |
| ✅ pytest | Testing framework |
| 🔍 basedpyright | Type checking |
| 📚 sphinx | Documentation generation |
# Install dev dependencies (includes Sphinx)
uv sync --group dev
# Run tests
just test
# Type check
just typecheck
# Format code
just fmt
# Build documentation
cd docs && make html- ✅ All tests must pass
- 🔍 Type checking must succeed
- 🧹 Code must be formatted with ruff
- 📝 Add tests for new features
- 📚 Update documentation as needed
This project uses Just as the preferred task runner for development workflows. Just recipes provide a convenient, consistent interface for common development tasks.
For contributors without Just installed, direct command alternatives are provided below.
| Just Recipe (Preferred) | Direct Command (Alternative) | Description |
|---|---|---|
just install |
uv sync --all-groups |
Install all dependencies |
just setup |
uv sync --all-groups |
Alias for install |
just lint |
uv run ruff check . |
Check code for issues |
just fmt |
uv run ruff format . |
Format code automatically |
just lint-fix |
uv run ruff check --fix . |
Lint and auto-fix issues |
just typecheck |
uv run ty check |
Run type checker |
just test |
uv run pytest |
Run tests (sequential) |
just test-parallel |
uv run pytest -n auto |
Run tests (parallel) |
just ci-checks |
(see note below) | Run all quality checks |
just docs |
uv run sphinx-build -b html docs docs/_build/html |
Build documentation |
just build |
uv build |
Build package distribution |
just clean |
(manual cleanup) | Clean build artifacts |
Note on just ci-checks: This recipe chains multiple commands with fail-fast behavior:
just install && just lint && just typecheck && just test-parallelIf running manually without Just, execute these commands in sequence and stop if any fails.
Storyville provides a pre-push Git hook to automatically run just ci-checks before pushing code. This prevents pushing
code that would fail CI checks.
Install the hook:
just enable-pre-pushDisable the hook:
just disable-pre-pushWhen enabled, the hook runs all quality checks (install, lint, typecheck, test-parallel) before each git push. If any
check fails, the push is aborted.
How it works:
- Creates
.git/hooks/pre-pushthat invokesjust ci-checks - The hook only runs locally (not shared via git)
- Each developer can enable/disable independently
- Disabling removes executable permission without deleting the hook
Tip: Enable this hook to catch issues before they reach CI, saving time and keeping the commit history clean.
You can test GitHub Actions workflows locally before pushing using the act tool:
Installation (macOS):
brew install actBasic Usage:
# Run the CI tests workflow locally
act -j ci_tests --rm
# Run all workflows
act --rmKnown Limitations:
- Caching behavior may differ from GitHub Actions
- Some GitHub-specific features may not work identically
- Secret handling requires additional configuration
- Docker must be running on your system
For more information, see the act documentation.
MIT License - see LICENSE for details.
| Resource | URL |
|---|---|
| 🏠 Repository | github.com/pauleveritt/storyville |
| 🐛 Issues | github.com/pauleveritt/storyville/issues |
| 📝 Discussions | github.com/pauleveritt/storyville/discussions |
| 🎯 tdom Project | github.com/pauleveritt/t-strings |
Made with 💜 by Paul Everitt
⭐ Star this repo if you find it useful! ⭐