sanic-routing
The AST-compiled node-tree router that powers Sanic's path and signal routing.
Repository Health
Technical Analysis
sanic-routing is the core routing engine that powers the Sanic async web framework, handling both HTTP path routing and internal signal routing since Sanic v21.3. Rather than walking a data structure at every request, it arranges registered routes into a hierarchical tree of path segments and compiles that tree directly into a specialized Python function using the ast module — trading a one-time compilation cost at startup for near-native dispatch speed on every incoming request.
The library ships a BaseRouter abstract class that application frameworks subclass to define their own get() method, plus a set of built-in path parameter types (str, int, float, uuid, slug, alpha, path, ymd, ext, and more) with pluggable custom-type registration via register_pattern(). It groups routes that share a path across different HTTP methods into RouteGroup instances, detects and resolves conflicts between static and dynamic route declarations, and supports strict-slash matching, route priorities, and Sanic’s <name:type> parameter syntax.
What You Get
- AST-compiled route matching - Routes are compiled into real Python source and executed via
compile()/exec(), not interpreted pattern-by-pattern at request time. - BaseRouter abstract class - Subclass it, implement
get(), and you have a working router; this is exactly how Sanic builds its own HTTP and signal routers. - Rich built-in parameter types - str, strorempty, int, float, uuid, slug, alpha, ymd (date), path, and ext (filename+extension) types out of the box, each with its own cast function and regex.
- Custom parameter type registration -
register_pattern()lets you add your own cast callable, matching regex, and optionalParamInfosubclass for domain-specific path segments. - Route grouping and conflict detection -
RouteGroupmerges routes sharing a path across different HTTP methods and raisesRouteExistson true duplicates, withoverwrite/appendescape hatches.
Common Use Cases
- Embedding Sanic’s router in a subclass - Framework authors subclass
BaseRouterto define an app-specificget()and reuse the same compiled-tree matching Sanic itself uses. - Type-safe path parameters - Declaring routes like
/users/<id:int>or/posts/<slug:slug>and getting parameters already cast to the right Python type on match. - Custom path parameter types - Registering a project-specific type (for example a
<version:semver>matcher) viaregister_pattern()without touching the core router. - High route-count APIs - Services with hundreds of routes benefit from tree-based dispatch instead of a linear scan through every registered pattern.
Under The Hood
Architecture
Execution centers on BaseRouter.finalize() (sanic_routing/router.py), which walks every registered RouteGroup in static_routes, dynamic_routes, and regex_routes, hands the non-static groups to Tree.generate() (sanic_routing/tree.py) to build a hierarchical Node tree keyed by path segment, and then calls _render() to walk that tree and emit Line objects (sanic_routing/line.py) that are joined into literal Python source, parsed with ast.parse, optionally passed through an experimental _optimize() AST-merging pass, and exec()’d so self._find_route becomes a real compiled function rather than an interpreted data structure. Route (route.py) and RouteGroup (group.py) are cleanly separated: a Route owns one handler/method-set/path, uses __slots__ for a small memory footprint, and normalizes/parses parameter syntax (<name:type>) in _setup_params; a RouteGroup clusters routes that share an identical path across different HTTP methods and is the actual unit stored in the routing dicts, with merge() enforcing conflict rules (raising RouteExists) and passing through shared properties from its first route via __getattr__. If the core tree-to-source compilation strategy in Node.to_src/Tree.render changed, essentially the whole matching path would need rewriting, since routing correctness depends on the exact indentation and conditional nesting the code generator produces.
Tech Stack
Pure-Python standard library only for the runtime code — ast, re, typing, __slots__-based classes instead of dataclasses, urllib.parse for quote/unquote, and uuid/datetime for two of the built-in parameter casts — with zero third-party runtime dependencies declared in setup.py. Packaging is classic setuptools (setup.py, no pyproject.toml build backend), supporting Python 3.7 through 3.10 per its classifiers. Development tooling is ruff (lint + format, configured in pyproject.toml) and mypy for type checking, run through tox across py37–py310 plus a dedicated lint environment, wired into GitHub Actions (.github/workflows/python-package.yml) as a version matrix and a separate python-publish.yml for PyPI releases.
Code Quality
The tests/ directory carries nine dedicated test modules exercising builtin and custom parameter types, the Line renderer, Node tree behavior, the generated router source, general routing utilities, and unquoting, all run with pytest under tox across four Python versions. Error handling is explicit and typed: Route/RouteGroup raise well-named exceptions (RouteExists, InvalidUsage, ParameterNameConflicts, FinalizationError) rather than swallowing failures, and parameter casting relies on ValueError as the designed control-flow signal that a segment doesn’t match a given type. Type hints are used throughout with typing, __slots__ constrains attribute surfaces on hot-path classes (Route), and mypy plus ruff check/ruff format --check are enforced in CI alongside the test matrix — a mature setup for a project of this size.
API Design
The public surface is intentionally small: subclass BaseRouter, implement get(), call add() for each route, then finalize() once — a handful of well-documented methods with docstrings on the trickier ones (register_pattern, finalize). Parameter typing follows Sanic’s existing <name:type> convention, which keeps the learning curve low for anyone already familiar with Sanic route declarations, though the introspection-heavy internals (find_route_src, the AST optimizer, the globals() injection in register_pattern) assume a maintainer-level audience rather than a beginner one. There is no interactive tutorial beyond the README and a single example/basic.py, so ramping up on the compiled-tree internals in depth still requires reading the source.