Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/starlette/_utils.py: 47%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import functools
4import re
5import sys
6from collections.abc import AsyncGenerator, Awaitable, Callable, Generator
7from contextlib import AbstractAsyncContextManager, asynccontextmanager
8from dataclasses import dataclass
9from ipaddress import AddressValueError, IPv6Address
10from typing import Any, Generic, Protocol, TypeVar, overload
12import anyio.abc
14from starlette.types import Scope
16if sys.version_info >= (3, 13): # pragma: no cover
17 from inspect import iscoroutinefunction
18 from typing import TypeIs
19else: # pragma: no cover
20 from asyncio import iscoroutinefunction
22 from typing_extensions import TypeIs
25T = TypeVar("T")
26AwaitableCallable = Callable[..., Awaitable[T]]
28# Reject characters that could make a Host header change the URL path or authority.
29_HOST_RE = re.compile(
30 r"^(?P<host>[a-z0-9._~%!$&'()*+,;=-]+|\[(?:(?P<ipv6>[a-f0-9]*:[a-f0-9.:]+)|"
31 r"(?-i:v)[a-f0-9]+\.[a-z0-9._~!$&'()*+,;=:-]+)\])(?::(?P<port>[0-9]+))?$",
32 re.IGNORECASE,
33)
36@dataclass(frozen=True, slots=True)
37class ParsedHost:
38 host: str
39 port: str | None
41 @property
42 def authority(self) -> str:
43 return self.host if self.port is None else f"{self.host}:{self.port}"
45 @property
46 def is_valid_port(self) -> bool:
47 if self.port is None:
48 return True
49 port = self.port.lstrip("0")
50 return len(port) <= 5 and int(port or "0") <= 65535
53@overload
54def is_async_callable(obj: AwaitableCallable[T]) -> TypeIs[AwaitableCallable[T]]: ...
57@overload
58def is_async_callable(obj: Any) -> TypeIs[AwaitableCallable[Any]]: ...
61def is_async_callable(obj: Any) -> Any:
62 while isinstance(obj, functools.partial):
63 obj = obj.func
65 return iscoroutinefunction(obj) or (callable(obj) and iscoroutinefunction(obj.__call__))
68T_co = TypeVar("T_co", covariant=True)
71class AwaitableOrContextManager(
72 Awaitable[T_co], AbstractAsyncContextManager[T_co], Protocol[T_co]
73): ... # pragma: no branch
76class SupportsAsyncClose(Protocol):
77 async def close(self) -> None: ... # pragma: no cover
80SupportsAsyncCloseType = TypeVar("SupportsAsyncCloseType", bound=SupportsAsyncClose, covariant=False)
83class AwaitableOrContextManagerWrapper(Generic[SupportsAsyncCloseType]):
84 __slots__ = ("aw", "entered")
86 def __init__(self, aw: Awaitable[SupportsAsyncCloseType]) -> None:
87 self.aw = aw
89 def __await__(self) -> Generator[Any, None, SupportsAsyncCloseType]:
90 return self.aw.__await__()
92 async def __aenter__(self) -> SupportsAsyncCloseType:
93 self.entered = await self.aw
94 return self.entered
96 async def __aexit__(self, *args: Any) -> None | bool:
97 await self.entered.close()
98 return None
101@asynccontextmanager
102async def create_collapsing_task_group() -> AsyncGenerator[anyio.abc.TaskGroup, None]:
103 try:
104 async with anyio.create_task_group() as tg:
105 yield tg
106 except BaseExceptionGroup as excs:
107 if len(excs.exceptions) != 1:
108 raise
110 exc = excs.exceptions[0]
111 context = None if exc.__suppress_context__ else exc.__context__
112 raise exc from exc.__cause__ or context
115def parse_host_header(host_header: str | None) -> ParsedHost | None:
116 """Parse `host_header` into its host and port components.
118 The host preserves brackets around IP literals. Invalid headers produce `None`.
119 """
120 if host_header is None:
121 return None
123 match = _HOST_RE.fullmatch(host_header)
124 if match is None:
125 return None
127 ipv6 = match["ipv6"]
128 if ipv6 is not None:
129 try:
130 IPv6Address(ipv6)
131 except AddressValueError:
132 return None
134 return ParsedHost(match["host"], match["port"])
137def get_route_path(scope: Scope) -> str:
138 path: str = scope["path"]
139 root_path = scope.get("root_path", "")
140 if not root_path:
141 return path
143 if not path.startswith(root_path):
144 return path
146 if path == root_path:
147 return ""
149 if path[len(root_path)] == "/":
150 return path[len(root_path) :]
152 return path