Easily rewrite the static asset paths in your tdom-based markup. Works great for static-site generators. Works with package assets.
- Wrap a component with a decorator
- Decorator finds
<link>and replaces withTraversableElementthat has the path - Later, string rendering resolves path
tdom-path provides utilities for resolving component static assets (CSS, JS, images) in component-based
web applications. It supports both package assets (using package:path syntax) and relative paths,
returning Traversable objects that represent resource locations suitable for web rendering.
Key Use Cases:
- Static site generators (SSG) with component-based architectures
- Component libraries with embedded static assets
- Framework-independent asset resolution
- Package asset management for reusable components
- Package Asset Support: Reference assets from installed packages using
package:asset/pathsyntax - Component Asset Resolution: Pass component classes/instances directly to resolve local assets
- Tree Rewriting: Automatically transform
<link>and<script>elements to use Traversable - Decorator Support: Use
@path_nodesdecorator for automatic tree transformation - Flexible Path Formats: Supports package paths, relative paths with
./or../, and plain paths - Cross-Platform: Traversable ensures consistent resource access across platforms
- Type Safety: Comprehensive type hints with IDE autocomplete and type checking support
- Free Threaded: Written and tested to be free-threading friendly
- Asset Validation: Automatic validation that referenced assets exist (fail-fast with clear errors)
- Simple API: Clean functions for both manual and automatic use cases
- Framework Independence: Same components work in Flask, Django, FastAPI, Sphinx
$ uv add tdom-pathOr using pip:
$ pip install tdom-path- Python 3.14+
- tdom >= 0.1.13
Functional component with decorator and relative path:
from tdom import html, Node
from tdom_path import path_nodes
@path_nodes
def Head():
return html(t"""
<head>
<link rel="stylesheet" href="static/styles.css">
</head>
""")Functional component that manually applies (not decorator):
from tdom import html, Node
from tdom_path import make_path_nodes
def Head():
return make_path_nodes(html(t"""
<head>
<link rel="stylesheet" href="static/styles.css">
</head>
"""), Head)Functional component with decorator and package asset path:
from tdom import html, Node
from tdom_path import path_nodes
@path_nodes
def Head():
return html(t"""
<head>
<link rel="stylesheet" href="mypackage:static/styles.css">
</head>
""")Class-based component:
from dataclasses import dataclass
from tdom import html, Node
from tdom_path import path_nodes
@dataclass
class Head:
@path_nodes
def __call__(self):
return html(t"""
<head>
<link rel="stylesheet" href="static/styles.css">
</head>
""")How Path Type Detection Works:
- If the path contains a colon (
:), it's treated as a package path - Otherwise, it's treated as a relative path (relative to the component's module)
- No need to specify the type explicitly - detection is automatic
Supported Relative Path Formats:
static/styles.css- Plain relative path./static/styles.css- Explicit current directory../shared/utils.css- Parent directory navigation
Here's a complete example showing the full pipeline from component to rendered HTML:
from pathlib import PurePosixPath
from tdom import html
from tdom_path import make_path_nodes, render_path_nodes
from tdom_path.tree import RelativePathStrategy
# 1. Define your component with mixed package and relative paths
class Heading:
def __html__(self):
return html(t'''
<div class="heading">
<link rel="stylesheet" href="bootstrap:dist/css/bootstrap.css">
<link rel="stylesheet" href="static/heading.css">
<script src="static/heading.js"></script>
<h1>Welcome</h1>
</div>
''')
# 2. Transform string paths to Traversable (package and component-relative)
heading = Heading()
tree = heading.__html__()
path_tree = make_path_nodes(tree, heading)
# Bootstrap link href is now: Traversable for bootstrap package
# Heading link href is now: Traversable for component's static/heading.css
# Script src is now: Traversable for component's static/heading.js
# 3. Render for a specific target page (relative paths)
target = PurePosixPath("mysite/pages/about.html")
rendered_tree = render_path_nodes(path_tree, target)
# Paths are now calculated relative to target location
# 4. Convert to HTML string
html_output = str(rendered_tree)Use the package:path syntax to reference assets from installed Python packages:
# Basic syntax: "package_name:resource/path"
"mypackage:static/styles.css" # Asset from mypackage
"bootstrap:dist/css/bootstrap.css" # Asset from bootstrap package
"my.package:images/logo.png" # Asset from my.package (dotted names supported)
# The component parameter is ignored for package paths
make_traversable(None, "mypackage:static/styles.css") # component=None is fine
make_traversable(Heading, "mypackage:static/styles.css") # component is ignoredPackage Path Detection:
- Any path containing a colon (
:) is treated as a package path - Package name is extracted from the left side of the colon
- Resource path is extracted from the right side of the colon
Reference assets relative to the component's module location:
# Plain relative path (no prefix)
"static/styles.css"
# Explicit current directory
"./static/styles.css"
# Parent directory navigation
"../shared/utils.css"
# All require a component with __module__ attribute
make_traversable(Heading, "static/styles.css") # Uses Heading's module locationRelative Path Detection:
- Any path without a colon (
:) is treated as a relative path - Resolved relative to the component's
__module__location - Component parameter is required (must have
__module__attribute)
All asset paths are automatically validated during tree transformation:
from tdom_path import make_path_nodes
# If an asset doesn't exist, transformation fails immediately
tree = html(t'''
<head>
<link rel="stylesheet" href="mypackage:static/missing.css">
</head>
''')
try:
make_path_nodes(tree, Heading)
except FileNotFoundError as e:
# Error message includes:
# - Asset filename
# - Attribute name (href/src)
# - Component context
# - Full path for debugging
print(e) # "Asset not found: 'missing.css' (attribute: 'href', component: 'Heading'...)"Validation Features:
- Fail-fast: Errors raised immediately when asset not found
- Clear error messages with full context
- Includes component and attribute information for debugging
The tree rewriting functionality walks a tdom Node tree and automatically detects elements with static asset
references (<link> and <script> tags), converting their href/src attribute values from strings to Traversable
objects.
<link>tags withhrefattribute (anywhere in tree)<script>tags withsrcattribute (anywhere in tree)- Both package paths and relative paths are transformed
External URLs and special schemes are left unchanged:
- External URLs:
http://,https://,// - Special schemes:
mailto:,tel:,data:,javascript: - Anchor-only links:
#...
tdom-path is highly optimized for real-world SSG workflows with 17.9x speedup for cached module accesses.
- Path resolution: 25.8μs (cold) → 1.4μs (warm) - 17.9x faster
- Building 100 pages: 2.5ms (no cache) → 0.16ms (with cache) - 94% faster
- Uses
@lru_cachefor module loading optimization - Zero overhead on first use, massive speedup on repeated use
just benchmark # Run standalone benchmark
just test -m slow # Run pytest-based performance testsSee the Performance Guide for detailed analysis, profiling tools, and optimization tips.
def make_traversable(component: Any, asset: str) -> TraversableCreate path to asset resource as a Traversable instance.
Supports two path formats:
-
Package paths:
"package:resource/path"(e.g.,"mypackage:static/styles.css")- Resolves using
importlib.resources.files()to access package resources - Works with any installed package
- Component parameter is ignored for package paths
- Resolves using
-
Relative paths:
"resource/path"or"./resource/path"or"../resource/path"- Resolved relative to the component's module
- Uses the component's
__module__attribute to determine the base location - Component parameter is required (must have
__module__)
Path type detection is automatic based on presence of colon (:) character.
Parameters:
component: Python object with__module__attribute (class, function, instance, etc.) For package paths, this parameter is ignored and can be None.asset: Path to the asset. Can be:- Package path:
"package:resource/path" - Relative path:
"resource/path","./resource/path", or"../resource/path"
- Package path:
Returns:
Traversableinstance representing the resource location
Raises:
TypeError: If component doesn't have__module__attribute (relative paths only)ModuleNotFoundError: If a package path references a non-existent packageImportError: If there's an issue importing a package
Examples:
# Package path - access asset from installed package
pkg_path = make_traversable(None, "mypackage:static/styles.css")
# Relative path - access local component asset
css_path = make_traversable(Heading, "static/styles.css")
# With ./ prefix
css_path = make_traversable(Heading, "./static/styles.css")
# Parent directory
shared_path = make_traversable(Heading, "../shared/common.css")def make_path_nodes(target: Node, component: Any) -> NodeRewrite asset-bearing attributes in a tdom tree to use make_path.
Walks the Node tree and detects elements with static asset references (<link> and <script> tags), converting their
href/src string attributes to Traversable using make_traversable(component, attr_value).
Automatically validates that all assets exist, raising FileNotFoundError immediately if any asset is not found.
Parameters:
target: Root node of the tree to processcomponent: Component instance/class for make_traversable() resolution (used for relative paths only)
Returns:
- New Node tree with asset attributes converted to Traversable (immutable transformation)
Raises:
FileNotFoundError: If any referenced asset doesn't existModuleNotFoundError: If a package path references a non-existent package
Examples:
from tdom import Element
from mysite.components.heading import Heading
# Create tree with mixed package and relative paths
tree = Element("head", children=[
Element("link", {"rel": "stylesheet", "href": "bootstrap:dist/css/bootstrap.css"}),
Element("link", {"rel": "stylesheet", "href": "static/styles.css"})
])
# Transform to use Traversable
new_tree = make_path_nodes(tree, Heading)@path_nodesDecorator to automatically apply make_path_nodes to component output.
Supports both function components and class component methods. For function components, uses the function itself as the
component. For class methods (__call__ or __html__), uses self as the component.
Examples:
# Function component
@path_nodes
def heading(text: str) -> Element:
return Element("link", {"href": "mypackage:static/styles.css"})
# Class component
class Heading:
@path_nodes
def __html__(self) -> Element:
return Element("link", {"href": "static/styles.css"})def render_path_nodes(
tree: Node,
target: PurePosixPath,
strategy: RenderStrategy | None = None
) -> NodeRender TraversableElement nodes to Element nodes with relative path strings.
Walks the Node tree, detects TraversableElement instances containing Traversable attribute values, and transforms them into regular Element instances with those paths rendered as strings using the provided strategy.
This is the final rendering step after make_path_nodes() has converted asset paths to Traversable instances. It
calculates the appropriate string representation for each path based on the target output location.
Parameters:
tree: Root node of the tree to processtarget: PurePosixPath target output location (e.g.,"mysite/pages/index.html")strategy: Optional RenderStrategy for path calculation. Defaults toRelativePathStrategy()if None.
Returns:
- New Node tree with TraversableElement nodes transformed to Element nodes containing string path attributes
Examples:
from pathlib import PurePosixPath
from tdom import html
from tdom_path import make_path_nodes, render_path_nodes
from tdom_path.tree import RelativePathStrategy
from mysite.components.heading import Heading
# Step 1: Create tree with string asset paths
tree = html(t'''
<head>
<link rel="stylesheet" href="mypackage:static/styles.css">
</head>
''')
# Step 2: Transform to TraversableElement with Traversable
path_tree = make_path_nodes(tree, Heading)
# Step 3: Render to Element with relative path strings
target = PurePosixPath("mysite/pages/about.html")
rendered = render_path_nodes(path_tree, target)
# With site prefix for subdirectory deployment
strategy = RelativePathStrategy(site_prefix=PurePosixPath("mysite/static"))
rendered = render_path_nodes(path_tree, target, strategy=strategy)@dataclass(frozen=True, slots=True)
class RelativePathStrategy:
site_prefix: PurePosixPath | None = NoneStrategy for rendering paths as relative URLs.
Calculates relative paths from the target output location to the source asset location, optionally prepending a site prefix for deployment scenarios where assets are served from a subdirectory.
Parameters:
site_prefix: Optional PurePosixPath prefix to prepend to all calculated paths (e.g.,PurePosixPath("mysite/static"))
Examples:
from pathlib import PurePosixPath
from tdom_path.tree import RelativePathStrategy
from tdom_path.webpath import make_path
from mysite.components.heading import Heading
# Basic relative path calculation
strategy = RelativePathStrategy()
source = make_traversable(Heading, "static/styles.css")
target = PurePosixPath("mysite/pages/about.html")
path_str = strategy.calculate_path(source, target)
# With site prefix for subdirectory deployment
strategy = RelativePathStrategy(site_prefix=PurePosixPath("mysite/static"))
path_str = strategy.calculate_path(source, target)class RenderStrategy(Protocol):
def calculate_path(self, source: Traversable, target: PurePosixPath) -> str:
...Protocol for path rendering strategies.
Defines the interface for calculating how Traversable paths should be rendered as strings in the final HTML output. Implementations can provide different rendering strategies such as relative paths, absolute paths, CDN URLs, etc.
Extensibility: Create custom strategies by implementing the protocol:
from pathlib import PurePosixPath
from importlib.resources.abc import Traversable
from tdom_path.tree import RenderStrategy
class AbsolutePathStrategy:
"""Render all paths as absolute URLs."""
def __init__(self, base_url: str):
self.base_url = base_url
def calculate_path(self, source: Traversable, target: PurePosixPath) -> str:
return f"{self.base_url}/{source}"
# Use custom strategy
strategy = AbsolutePathStrategy("https://cdn.example.com")
rendered = render_path_nodes(path_tree, target, strategy=strategy)Current Phase: Phase 4 - Traversable and Package Paths (Complete)
- ✅
make_traversable()function for component asset resolution - ✅ Converts Python module names to web paths
- ✅ Returns
Traversablefor resource access - ✅ Handles repeated module names (e.g.,
heading.heading→heading) - ✅ Works with classes, instances, and any object with
__module__ - ✅ Type safe with comprehensive type hints
- ✅ 15 focused tests covering core functionality
- ✅ 100% test coverage on webpath.py
- ✅ ty type checker compliance
- ✅
make_path_nodes()function for tree transformation - ✅
@path_nodesdecorator for automatic transformation - ✅ Supports both function and class components
- ✅ Detects
<link>tags anywhere in tree - ✅ Detects
<script>tags anywhere in tree - ✅ Skips external URLs and special schemes
- ✅ Immutable tree transformation (creates new tree)
- ✅ Comprehensive tests
- ✅ Type safe with ty compliance
- ✅
render_path_nodes()function for final HTML rendering - ✅
RenderStrategyProtocol for extensible rendering strategies - ✅
RelativePathStrategywith optional site_prefix support - ✅ Transforms TraversableElement to Element with string paths
- ✅ Calculates relative paths from target to source
- ✅ Processes any Traversable attribute (not just href/src)
- ✅ Tree walking helper
_walk_tree()for reusable traversal - ✅ Immutable transformations with optimization
- ✅ Type safe with ty compliance
- ✅ Package asset support with
package:pathsyntax - ✅ Colon-based automatic path type detection
- ✅
Traversableas primary return type - ✅ Support for relative paths (plain,
./,../) - ✅ Package path parsing and resolution
- ✅ Asset existence validation with fail-fast errors
- ✅ Test fixtures for package resources
- ✅ 27 tests covering package and relative path workflows
- ✅ Integration tests for end-to-end pipelines
- ✅ Type safe with Traversable support
- Simple and Focused: Direct function APIs for resolving component assets
- Resource-Oriented: Traversable paths for both package and file resources
- Package-First: Native support for
package:pathsyntax - Cross-Platform: Traversable ensures consistent resource access
- Web-First Design: Paths designed for HTML rendering and resource access
- Component-centric: Pass component objects directly, framework extracts
__module__ - Type Safety First: Comprehensive type hints enable excellent IDE support
- Immutable by Default: Tree transformations create new structures
- Fail-Fast Validation: Clear errors when assets don't exist
- Keep It Simple: No complex policies or abstractions - direct resource resolution
# Run tests
just test
# Run tests in parallel
just test-parallel
# Run all quality checks (lint, format, typecheck, test)
just ci-checks
# Type checking with ty
just typecheckContributions are welcome! Please see the project repository for contribution guidelines.
[License information will be added]