Route Discovery¶
The discovery module extracts route information from ASGI applications. It provides framework-specific extractors for Litestar, Starlette, FastAPI, and can also extract routes from OpenAPI schemas.
Overview¶
Route discovery is the first step in pytest-routes’ testing pipeline:
Detection - Identify the framework (Litestar, FastAPI, Starlette)
Extraction - Pull route definitions from the application
Normalization - Convert to
RouteInfoobjects with metadata
The get_extractor() factory function automatically selects the
appropriate extractor based on your application type.
Quick Start¶
The simplest way to extract routes:
from pytest_routes import get_extractor, RouteInfo
# Automatic framework detection
extractor = get_extractor(app)
# Extract all routes
routes: list[RouteInfo] = extractor.extract_routes(app)
for route in routes:
print(f"{route.methods} {route.path}")
print(f" Path params: {route.path_params}")
print(f" Query params: {route.query_params}")
Architecture¶
+------------------+
| get_extractor() | -- Factory function
+--------+---------+
|
v
+------------------+ +-------------------+
| RouteExtractor |<----| LitestarExtractor |
| (Protocol) | +-------------------+
+------------------+ +-------------------+
^ <-----| StarletteExtractor|
| +-------------------+
| +-------------------+
+----------<----| OpenAPIExtractor |
+-------------------+
API Reference¶
Core Types¶
RouteInfo¶
- class pytest_routes.discovery.base.RouteInfo[source]¶
Bases:
objectNormalized route information.
This dataclass represents a discovered route from an ASGI application, containing all metadata needed for property-based testing. It supports both HTTP routes and WebSocket endpoints through the is_websocket flag and optional websocket_metadata field.
- Variables:
path – The route path pattern (e.g., “/users/{user_id}”).
methods – HTTP methods for this route (e.g., [“GET”, “POST”]). For WebSocket routes, this is typically [“WEBSOCKET”] or empty.
name – Optional route name from the framework.
handler – Reference to the handler function/coroutine.
path_params – Mapping of path parameter names to their types.
query_params – Mapping of query parameter names to their types.
body_type – Type annotation for the request body, if applicable.
tags – Framework-assigned tags for grouping/categorization.
deprecated – Whether the route is marked as deprecated.
description – Documentation string for the route.
is_websocket – True if this is a WebSocket endpoint.
websocket_metadata – Additional WebSocket-specific configuration.
- Parameters:
The
RouteInfodataclass is the normalized representation of a route, containing all information needed for test generation.Attributes
pathThe URL path pattern (e.g.,
/users/{user_id})methodsList of HTTP methods (e.g.,
["GET", "POST"])nameOptional route name from the framework
handlerReference to the route handler function
path_paramsDict mapping parameter names to their types
query_paramsDict mapping query parameter names to their types
body_typeThe expected request body type (for POST/PUT/PATCH)
tagsOpenAPI tags for categorization
deprecatedWhether the route is marked deprecated
descriptionHuman-readable route description
Example
route = RouteInfo( path="/users/{user_id}", methods=["GET"], name="get_user", path_params={"user_id": int}, query_params={"include_posts": bool}, )
- get_websocket_metadata()[source]¶
Get WebSocket metadata, creating default if not set.
- Return type:
WebSocketMetadata- Returns:
WebSocketMetadata instance with route-specific or default values.
- Raises:
ValueError – If called on a non-WebSocket route.
- __init__(path, methods, name=None, handler=None, path_params=<factory>, query_params=<factory>, body_type=None, tags=<factory>, deprecated=False, description=None, is_websocket=False, websocket_metadata=None)¶
- Parameters:
RouteExtractor¶
Factory Function¶
- pytest_routes.discovery.get_extractor(app)[source]¶
Get the appropriate route extractor for an ASGI app.
- Parameters:
app (
Any) – The ASGI application.- Return type:
- Returns:
A RouteExtractor instance that can handle this app.
- Raises:
ValueError – If no suitable extractor is found.
Example
from litestar import Litestar from pytest_routes import get_extractor app = Litestar(route_handlers=[...]) extractor = get_extractor(app) # Returns LitestarExtractor routes = extractor.extract_routes(app)
Framework Extractors¶
Litestar Extractor¶
Litestar route extraction.
- class pytest_routes.discovery.litestar.LitestarExtractor[source]¶
Bases:
RouteExtractorExtract routes from Litestar applications.
This extractor provides first-class support for Litestar framework route discovery. It extracts comprehensive route metadata including path parameters, query parameters, request body types, and route metadata from Litestar applications.
- The extractor handles both the old and new Litestar API versions, supporting:
Path parameter extraction with type annotations
Query parameter detection from handler signatures
Request body type extraction from DTOs and type hints
Route metadata (tags, deprecation status, descriptions)
Example
>>> from litestar import Litestar, get >>> from pytest_routes.discovery.litestar import LitestarExtractor >>> >>> @get("/users/{user_id:int}") >>> async def get_user(user_id: int, include_posts: bool = False) -> dict: ... return {"id": user_id, "include_posts": include_posts} >>> >>> app = Litestar(route_handlers=[get_user]) >>> extractor = LitestarExtractor() >>> routes = extractor.extract_routes(app) >>> route = routes[0] >>> route.path '/users/{user_id:int}' >>> route.path_params {'user_id': <class 'int'>} >>> route.query_params {'include_posts': <class 'bool'>}
Note
The extractor automatically skips HEAD methods as they are typically auto-generated by the framework and don’t need explicit testing.
- supports(app)[source]¶
Check if the application is a Litestar instance.
- Parameters:
app (
Any) – The ASGI application to check.- Return type:
- Returns:
True if the app is a Litestar instance, False otherwise.
Note
Returns False if Litestar is not installed, allowing graceful degradation when the framework is not available.
- extract_routes(app)[source]¶
Extract all HTTP and WebSocket routes from a Litestar application.
This method traverses the Litestar route registry and extracts comprehensive metadata for each route including path parameters, query parameters, request body types, and route metadata. It handles both HTTP and WebSocket routes.
- Parameters:
app (
Any) – A Litestar application instance.- Returns:
path (route path pattern), methods (HTTP methods), name (handler name), handler (function reference), path_params (parameter name to type mapping), query_params (query parameter mapping), body_type (request body type), tags (route tags), deprecated (deprecation flag), description (route docs), is_websocket (True for WebSocket routes), websocket_metadata (WebSocket config).
- Return type:
A list of RouteInfo objects containing complete route metadata
Example
>>> from litestar import Litestar, get, post, websocket >>> from litestar.dto import DTOData >>> from dataclasses import dataclass >>> >>> @dataclass >>> class CreateUser: ... name: str ... email: str >>> >>> @get("/users/{user_id:int}") >>> async def get_user(user_id: int) -> dict: ... return {"id": user_id} >>> >>> @post("/users", tags=["users"]) >>> async def create_user(data: CreateUser) -> dict: ... return {"name": data.name} >>> >>> @websocket("/ws/chat") >>> async def chat_handler(socket) -> None: ... await socket.accept() ... await socket.send_json({"type": "welcome"}) >>> >>> app = Litestar(route_handlers=[get_user, create_user, chat_handler]) >>> extractor = LitestarExtractor() >>> routes = extractor.extract_routes(app) >>> len(routes) 3 >>> routes[0].path_params {'user_id': <class 'int'>} >>> routes[1].tags ['users'] >>> routes[2].is_websocket True
Note
HEAD methods are automatically skipped for HTTP routes
Handles both old API (handler directly) and new API (handler tuple)
Type hints are extracted from handler functions when available
Dependencies and framework injection parameters are filtered out
WebSocket routes have auto_accept=True in their metadata
The Litestar extractor provides first-class support for Litestar applications, extracting full type information from route handlers including:
Path parameters with type annotations
Query parameters with defaults
Request body models (Pydantic, dataclasses, msgspec)
OpenAPI metadata (tags, deprecation, description)
from litestar import Litestar, get
from pytest_routes.discovery.litestar import LitestarExtractor
@get("/users/{user_id:int}")
async def get_user(user_id: int) -> User:
...
app = Litestar([get_user])
extractor = LitestarExtractor()
if extractor.supports(app):
routes = extractor.extract_routes(app)
Starlette Extractor¶
Starlette/FastAPI route extraction.
- class pytest_routes.discovery.starlette.StarletteExtractor[source]¶
Bases:
RouteExtractorExtract routes from Starlette and FastAPI applications.
This extractor provides comprehensive route discovery for Starlette-based frameworks including vanilla Starlette and FastAPI applications. It handles route mounts, path parameter parsing, and query parameter extraction.
- The extractor supports:
Recursive route collection through Mount instances
Path parameter extraction with type conversion (int, float, path)
Query parameter detection from endpoint signatures
FastAPI-specific features (BaseModel bodies, dependency injection)
Starlette request/response parameter filtering
Example
>>> from starlette.applications import Starlette >>> from starlette.routing import Route >>> >>> async def get_user(request): ... user_id = request.path_params["user_id"] ... return JSONResponse({"id": user_id}) >>> >>> app = Starlette(routes=[Route("/users/{user_id:int}", get_user, methods=["GET"])]) >>> extractor = StarletteExtractor() >>> routes = extractor.extract_routes(app) >>> routes[0].path_params {'user_id': <class 'int'>}
Note
HEAD methods are automatically filtered out
Mount instances are recursively traversed with path prefix accumulation
FastAPI BaseModel parameters are detected and skipped from query params
- supports(app)[source]¶
Check if the application is a Starlette or FastAPI instance.
- Parameters:
app (
Any) – The ASGI application to check.- Return type:
- Returns:
True if the app is a Starlette or FastAPI instance, False otherwise.
Note
Checks for both Starlette and FastAPI classes independently, returning False if neither framework is installed. This allows graceful degradation when frameworks are not available.
- extract_routes(app)[source]¶
Extract all HTTP and WebSocket routes from a Starlette or FastAPI application.
This method traverses the application’s route registry, recursively handling Mount instances to collect all routes with their full path prefixes. It extracts path parameters, query parameters, and route metadata for both HTTP and WebSocket routes.
- Parameters:
app (
Any) – A Starlette or FastAPI application instance.- Returns:
path (full route path including mount prefixes), methods (HTTP methods or “WEBSOCKET”), name (route name), handler (endpoint function), path_params (parameter name to type mapping parsed from path), query_params (query parameter mapping), body_type (always None for Starlette - use OpenAPI extractor), is_websocket (True for WebSocket routes), websocket_metadata (WebSocket config).
- Return type:
A list of RouteInfo objects containing route metadata
Example
>>> from fastapi import FastAPI, Query, WebSocket >>> from pydantic import BaseModel >>> >>> app = FastAPI() >>> >>> class User(BaseModel): ... name: str ... email: str >>> >>> @app.get("/users/{user_id}") >>> async def get_user(user_id: int, include_posts: bool = Query(False)): ... return {"id": user_id, "include_posts": include_posts} >>> >>> @app.post("/users") >>> async def create_user(user: User): ... return {"name": user.name} >>> >>> @app.websocket("/ws/chat") >>> async def websocket_endpoint(websocket: WebSocket): ... await websocket.accept() ... await websocket.send_json({"type": "welcome"}) >>> >>> extractor = StarletteExtractor() >>> routes = extractor.extract_routes(app) >>> len(routes) 3 >>> routes[0].path_params {'user_id': <class 'int'>} >>> routes[0].query_params {'include_posts': <class 'bool'>} >>> routes[2].is_websocket True
Note
Recursively processes Mount instances to handle sub-applications
HEAD methods are automatically filtered out for HTTP routes
Path prefixes from Mount instances are accumulated
Query parameter extraction handles FastAPI Query/Body annotations
WebSocket routes have auto_accept=False in their metadata (requires manual accept)
The Starlette extractor works with both Starlette and FastAPI applications. Since FastAPI is built on Starlette, this extractor handles both frameworks.
from fastapi import FastAPI
from pytest_routes.discovery.starlette import StarletteExtractor
app = FastAPI()
@app.get("/users/{user_id}")
async def get_user(user_id: int) -> dict:
...
extractor = StarletteExtractor()
routes = extractor.extract_routes(app)
OpenAPI Extractor¶
OpenAPI schema-based route extraction.
- class pytest_routes.discovery.openapi.OpenAPIExtractor[source]¶
Bases:
RouteExtractorExtract routes from an OpenAPI schema.
This extractor provides framework-agnostic route discovery by parsing OpenAPI (Swagger) schemas. It supports both pre-loaded schemas and runtime schema extraction from Litestar and FastAPI applications.
The extractor provides comprehensive type conversion from JSON Schema to Python types, including primitive types (string, integer, number, boolean), complex types (objects converted to dataclasses), container types (arrays with item types), reference resolution ($ref support), format-based types (date-time, uuid, email), and schema composition (allOf, oneOf, anyOf).
Example
>>> from fastapi import FastAPI >>> from pydantic import BaseModel >>> >>> app = FastAPI() >>> >>> class User(BaseModel): ... name: str ... email: str >>> >>> @app.post("/users/{user_id}") >>> async def update_user(user_id: int, user: User): ... return {"id": user_id, "name": user.name} >>> >>> extractor = OpenAPIExtractor() >>> routes = extractor.extract_routes(app) >>> route = routes[0] >>> route.path_params {'user_id': <class 'int'>} >>> route.body_type.__name__ 'User'
- Variables:
schema – Pre-loaded OpenAPI schema dict (optional)
_type_cache – Cache of generated dataclass types by name
_generated_type_counter – Counter for unique auto-generated type names
Note
Caches generated dataclass types to avoid duplicates. Supports OpenAPI 3.0+ schemas. Falls back to framework schema extraction if no schema provided.
- __init__(schema=None)[source]¶
Initialize the OpenAPI extractor with an optional pre-loaded schema.
- Parameters:
schema (
dict[str,Any] |None) – An optional pre-loaded OpenAPI schema dictionary. If not provided, the extractor will attempt to extract the schema from the application at runtime using framework-specific methods (Litestar’s openapi_schema or FastAPI’s openapi()).
Example
>>> # Using pre-loaded schema >>> schema = { ... "openapi": "3.0.0", ... "paths": { ... "/users/{user_id}": { ... "get": { ... "parameters": [ ... {"name": "user_id", "in": "path", "schema": {"type": "integer"}} ... ] ... } ... } ... }, ... } >>> extractor = OpenAPIExtractor(schema=schema) >>> >>> # Using runtime extraction >>> extractor = OpenAPIExtractor() >>> routes = extractor.extract_routes(app) # Schema extracted from app
- supports(app)[source]¶
Check if an OpenAPI schema can be extracted from the application.
- This method checks if the extractor can work with the given application by:
Checking if a pre-loaded schema was provided during initialization
Checking if the app has an ‘openapi_schema’ attribute (Litestar)
Checking if the app has an ‘openapi’ method (FastAPI)
- Parameters:
app (
Any) – The ASGI application to check for OpenAPI schema support.- Return type:
- Returns:
True if an OpenAPI schema is available or can be extracted, False otherwise.
Example
>>> from fastapi import FastAPI >>> from litestar import Litestar >>> >>> # FastAPI support >>> fastapi_app = FastAPI() >>> extractor = OpenAPIExtractor() >>> extractor.supports(fastapi_app) True >>> >>> # Litestar support >>> litestar_app = Litestar(route_handlers=[]) >>> extractor.supports(litestar_app) True >>> >>> # Pre-loaded schema >>> schema = {"openapi": "3.0.0", "paths": {}} >>> extractor_with_schema = OpenAPIExtractor(schema=schema) >>> extractor_with_schema.supports(None) # Any app works with pre-loaded schema True
Note
Always returns True if a schema was provided during initialization, regardless of the application type.
- extract_routes(app)[source]¶
Extract all routes from an OpenAPI schema.
This method parses the OpenAPI schema’s paths section and converts each operation into a RouteInfo object. It handles parameter extraction, request body type generation, and metadata extraction.
- Parameters:
app (
Any) – The ASGI application to extract routes from. If a pre-loaded schema was provided during initialization, this parameter is ignored and the pre-loaded schema is used instead.- Returns:
path (route path pattern), methods (HTTP method), name (operation ID), handler (always None for schema-based extraction), path_params (parameter name to type mapping), query_params (query parameter mapping), body_type (dataclass representing request body), tags (OpenAPI tags), deprecated (deprecation flag), and description (operation summary).
- Return type:
A list of RouteInfo objects containing complete route metadata
- Raises:
ValueError – If no schema was provided and the app doesn’t support OpenAPI schema extraction.
Example
>>> from fastapi import FastAPI >>> from pydantic import BaseModel >>> >>> app = FastAPI() >>> >>> class CreateUser(BaseModel): ... name: str ... email: str >>> >>> @app.post("/users", tags=["users"], deprecated=False) >>> async def create_user(user: CreateUser): ... return {"name": user.name} >>> >>> @app.get("/users/{user_id}", summary="Get a user") >>> async def get_user(user_id: int, include_posts: bool = False): ... return {"id": user_id} >>> >>> extractor = OpenAPIExtractor() >>> routes = extractor.extract_routes(app) >>> len(routes) 2 >>> post_route = routes[0] >>> post_route.methods ['POST'] >>> post_route.tags ['users'] >>> post_route.body_type.__name__ 'CreateUser' >>> get_route = routes[1] >>> get_route.path_params {'user_id': <class 'int'>} >>> get_route.query_params {'include_posts': <class 'bool'>} >>> get_route.description 'Get a user'
Note
Only extracts standard HTTP methods (GET, POST, PUT, PATCH, DELETE). Schema references ($ref) are automatically resolved. Complex request body schemas are converted to dataclasses. Generated dataclass types are cached to avoid duplicates. Uses pre-loaded schema if available, otherwise extracts from app.
The OpenAPI extractor parses an OpenAPI specification (JSON or YAML) to extract route information. This is useful for:
Testing against an API specification before implementation
Testing third-party APIs
Framework-agnostic route extraction
from pytest_routes.discovery.openapi import OpenAPIExtractor
openapi_schema = {
"openapi": "3.0.0",
"paths": {
"/users/{user_id}": {
"get": {
"parameters": [
{"name": "user_id", "in": "path", "schema": {"type": "integer"}}
]
}
}
}
}
extractor = OpenAPIExtractor(openapi_schema)
routes = extractor.extract_routes(None) # Schema-based, no app needed
Custom Extractors¶
Implement custom extractors for unsupported frameworks:
from pytest_routes.discovery.base import RouteExtractor, RouteInfo
from typing import Any
class MyFrameworkExtractor(RouteExtractor):
"""Extractor for MyFramework applications."""
def supports(self, app: Any) -> bool:
"""Check if this is a MyFramework app."""
return hasattr(app, "my_framework_marker")
def extract_routes(self, app: Any) -> list[RouteInfo]:
"""Extract routes from MyFramework app."""
routes = []
for route_def in app.get_routes():
routes.append(RouteInfo(
path=route_def.path,
methods=route_def.methods,
path_params=self._extract_path_params(route_def),
query_params=self._extract_query_params(route_def),
body_type=route_def.body_model,
))
return routes
See Also¶
Strategy Generation - How extracted routes are used for test generation
Test Execution - Running tests against discovered routes
API Reference - Complete API overview