Source code for responder.params

"""Type-hint-driven parameter markers for handler signatures.

Use these as parameter defaults to inject validated query parameters, headers,
cookies, or path parameters as handler arguments::

    from responder import Query, Header

    @api.route("/search")
    async def search(req, resp, *, q: str = Query(...), token: str = Header(None)):
        ...

``Query(...)`` (Ellipsis) marks a required parameter; ``Query(default)`` supplies
a default. Values are validated/coerced against the parameter's type annotation
with Pydantic; a failure yields a ``422``.
"""

from __future__ import annotations

import types
import weakref
from typing import Annotated, Any, NamedTuple, Union, get_args, get_origin

try:
    from pydantic import Field as _Field
    from pydantic import TypeAdapter as _TypeAdapter
except ImportError:  # pragma: no cover - pydantic is a core dep
    _TypeAdapter = None  # type: ignore[assignment,misc]
    _Field = None  # type: ignore[assignment]

__all__ = ["Query", "Header", "Cookie", "Path", "Form", "File", "Depends"]

# Pydantic ``Field`` constraints accepted on a marker. Anything else passed as a
# keyword is treated as a typo and rejected, rather than silently swallowed (a
# stray kwarg used to land in ``constraints`` and leave the parameter required).
_FIELD_CONSTRAINTS = frozenset(
    {
        "gt",
        "ge",
        "lt",
        "le",
        "multiple_of",
        "allow_inf_nan",
        "min_length",
        "max_length",
        "pattern",
        "max_digits",
        "decimal_places",
        "strict",
        "examples",
    }
)


class _Marker:
    """A parameter source marker, used as a handler-parameter default."""

    __slots__ = (
        "location",
        "default",
        "alias",
        "convert_underscores",
        "description",
        "deprecated",
        "constraints",
    )

    def __init__(
        self,
        location,
        default=...,
        *,
        alias=None,
        convert_underscores=True,
        description=None,
        deprecated=False,
        **constraints,
    ):
        unknown = set(constraints) - _FIELD_CONSTRAINTS
        if unknown:
            raise TypeError(
                f"{location.title()}() got unexpected keyword argument(s): "
                f"{', '.join(sorted(unknown))}. Valid constraints are: "
                f"{', '.join(sorted(_FIELD_CONSTRAINTS))}."
            )
        self.location = location
        self.default = default
        self.alias = alias
        self.convert_underscores = convert_underscores
        self.description = description
        self.deprecated = deprecated
        self.constraints = constraints

    @property
    def required(self):
        return self.default is ...

    def __repr__(self):
        return f"{self.location.title()}(default={self.default!r}, alias={self.alias!r})"


[docs] def Query(default=..., **kwargs): # noqa: N802 - marker factory, FastAPI-style """Mark a parameter as coming from the query string.""" return _Marker("query", default, **kwargs)
[docs] def Path(default=..., **kwargs): # noqa: N802 """Mark a parameter as coming from a path parameter.""" return _Marker("path", default, **kwargs)
[docs] def Form(default=..., **kwargs): # noqa: N802 """Mark a parameter as a form field (urlencoded or multipart body).""" return _Marker("form", default, **kwargs)
[docs] def File(default=..., **kwargs): # noqa: N802 """Mark a parameter as an uploaded file; injects an ``UploadFile`` (or a ``list`` of them for a sequence annotation).""" return _Marker("file", default, **kwargs)
class _Depends: """A dependency marker used as a handler-parameter default.""" __slots__ = ("provider",) def __init__(self, provider): if not callable(provider): raise TypeError("Depends() requires a callable provider") self.provider = provider def __eq__(self, other): # Two markers wrapping the same provider are interchangeable — this # lets ``include_router`` recognize fresh-but-identical ``Depends`` # lists when the same router is included at several prefixes. if not isinstance(other, _Depends): return NotImplemented return self.provider == other.provider def __hash__(self): return hash(self.provider) def __repr__(self): name = getattr(self.provider, "__name__", repr(self.provider)) return f"Depends({name})"
[docs] def Depends(provider): # noqa: N802 - marker factory, FastAPI-style """Inject the value returned by ``provider`` into a handler parameter. Unlike named ``api.dependency(...)`` providers, ``Depends`` is local to one handler and does not require registration on the app. """ return _Depends(provider)
def _is_sequence(annotation: Any) -> bool: origin = get_origin(annotation) # Unwrap Optional/Union (both ``Optional[list[int]]`` and ``list[int] | None``) # so an optional sequence parameter still binds repeated values as a list. if origin in (Union, types.UnionType): return any( arg is not type(None) and _is_sequence(arg) for arg in get_args(annotation) ) return annotation in (list, tuple, set, frozenset) or origin in ( list, tuple, set, frozenset, ) class ParamSpec(NamedTuple): """A resolved marker parameter: where to read it and how to validate it.""" name: str location: str annotation: Any marker: _Marker lookup: str # the request key (alias, or derived from the name) is_sequence: bool adapter: Any # a pydantic TypeAdapter, or None when unannotated @property def required(self) -> bool: return self.marker.required def _annotated_marker(annotation): """The ``_Marker`` carried in an ``Annotated`` annotation, if any. Also unwraps ``Optional[Annotated[...]]`` forms so older annotations or explicit ``Optional`` aliases do not hide marker metadata behind a ``Union``. """ for candidate in (annotation, *get_args(annotation)): for meta in getattr(candidate, "__metadata__", ()): if isinstance(meta, _Marker): return meta return None _PARAM_CACHE: weakref.WeakKeyDictionary = weakref.WeakKeyDictionary() def marker_params(handler: Any, hints: dict) -> tuple[ParamSpec, ...]: """The marker-driven ``ParamSpec``s for ``handler`` (cached per function).""" import inspect key = getattr(handler, "__func__", handler) try: return _PARAM_CACHE[key] except (KeyError, TypeError): pass # PEP 593 markers (``q: Annotated[int, Query()]``) carry the marker in the # annotation's metadata rather than the parameter default; resolve those once. # (The ``hints`` passed in already strip Annotated down to the inner type.) try: import typing extras = typing.get_type_hints(handler, include_extras=True) except Exception: extras = {} specs: list[ParamSpec] = [] params: Any try: params = inspect.signature(handler).parameters except (TypeError, ValueError): params = {} for pname, param in params.items(): marker = param.default if isinstance(param.default, _Marker) else None if marker is None: marker = _annotated_marker(extras.get(pname)) if marker is None: continue annotation = hints.get(pname, str) if marker.alias: lookup = marker.alias elif marker.location == "header" and marker.convert_underscores: lookup = pname.replace("_", "-") else: lookup = pname adapter = None # File uploads inject an UploadFile, not a coerced scalar — no adapter. if ( marker.location != "file" and _TypeAdapter is not None and annotation is not inspect.Parameter.empty ): target = annotation if marker.constraints and _Field is not None: target = Annotated[annotation, _Field(**marker.constraints)] try: adapter = _TypeAdapter(target) except Exception: adapter = None specs.append( ParamSpec( name=pname, location=marker.location, annotation=annotation, marker=marker, lookup=lookup, is_sequence=_is_sequence(annotation), adapter=adapter, ) ) result = tuple(specs) try: _PARAM_CACHE[key] = result except TypeError: pass return result def raw_value(spec: ParamSpec, request: Any, path_params: dict) -> Any: """Pull the raw (pre-validation) value for ``spec`` from the request. Returns ``...`` (Ellipsis) when the value is absent. ``request`` may be a Responder :class:`~responder.models.Request` or a Starlette ``WebSocket`` (whose handshake exposes ``query_params``/``headers``/``cookies`` with the same read API). """ loc = spec.location if loc == "query": params = getattr(request, "params", None) if params is None: # a WebSocket: query params live on the handshake params = request.query_params if spec.is_sequence: getter = getattr(params, "get_list", None) or params.getlist values = getter(spec.lookup) return values if values else ... return params.get(spec.lookup, ...) if loc == "header": return request.headers.get(spec.lookup, ...) if loc == "cookie": return request.cookies.get(spec.lookup, ...) if loc == "path": return path_params.get(spec.lookup, ...) return ... # pragma: no cover