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:

  1. Detection - Identify the framework (Litestar, FastAPI, Starlette)

  2. Extraction - Pull route definitions from the application

  3. Normalization - Convert to RouteInfo objects 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: object

Normalized 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 RouteInfo dataclass is the normalized representation of a route, containing all information needed for test generation.

Attributes

path

The URL path pattern (e.g., /users/{user_id})

methods

List of HTTP methods (e.g., ["GET", "POST"])

name

Optional route name from the framework

handler

Reference to the route handler function

path_params

Dict mapping parameter names to their types

query_params

Dict mapping query parameter names to their types

body_type

The expected request body type (for POST/PUT/PATCH)

tags

OpenAPI tags for categorization

deprecated

Whether the route is marked deprecated

description

Human-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},
)
path: str
methods: list[str]
name: str | None = None
handler: Callable[..., Any] | None = None
path_params: dict[str, type]
query_params: dict[str, type]
body_type: type | None = None
tags: list[str]
deprecated: bool = False
description: str | None = None
is_websocket: bool = False
websocket_metadata: WebSocketMetadata | None = None
property is_http: bool

Check if this is an HTTP route (not WebSocket).

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

class pytest_routes.discovery.base.RouteExtractor[source]

Bases: ABC

Abstract base for route extraction from ASGI apps.

Abstract base class that all extractors must implement.

abstractmethod extract_routes(app)[source]

Extract all routes from the application.

Parameters:

app (Any) – The ASGI application.

Return type:

list[RouteInfo]

Returns:

List of RouteInfo objects representing discovered routes.

abstractmethod supports(app)[source]

Check if this extractor supports the given app type.

Parameters:

app (Any) – The ASGI application to check.

Return type:

bool

Returns:

True if this extractor can handle the app.

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:

RouteExtractor

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: RouteExtractor

Extract 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:

bool

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: RouteExtractor

Extract 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:

bool

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: RouteExtractor

Extract 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.

Parameters:

schema (dict[str, Any] | None)

__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:
  1. Checking if a pre-loaded schema was provided during initialization

  2. Checking if the app has an ‘openapi_schema’ attribute (Litestar)

  3. Checking if the app has an ‘openapi’ method (FastAPI)

Parameters:

app (Any) – The ASGI application to check for OpenAPI schema support.

Return type:

bool

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