Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/starlette/testclient.py: 25%

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

315 statements  

1from __future__ import annotations 

2 

3import contextlib 

4import inspect 

5import io 

6import json 

7import math 

8import sys 

9import warnings 

10from collections.abc import Awaitable, Callable, Generator, Iterable, Mapping, MutableMapping, Sequence 

11from concurrent.futures import Future 

12from contextlib import AbstractContextManager 

13from types import GeneratorType 

14from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeGuard, cast 

15from urllib.parse import unquote, urljoin 

16 

17import anyio 

18import anyio.abc 

19import anyio.from_thread 

20from anyio.streams.stapled import StapledObjectStream 

21 

22from starlette._utils import is_async_callable 

23from starlette.exceptions import StarletteDeprecationWarning 

24from starlette.types import ASGIApp, Message, Receive, Scope, Send 

25from starlette.websockets import WebSocketDisconnect 

26 

27if sys.version_info >= (3, 11): # pragma: no cover 

28 from typing import Self 

29else: # pragma: no cover 

30 from typing_extensions import Self 

31 

32if TYPE_CHECKING: 

33 import httpx2 as httpx 

34else: 

35 try: 

36 import httpx2 as httpx 

37 except ModuleNotFoundError: # pragma: no cover 

38 try: 

39 import httpx 

40 except ModuleNotFoundError: 

41 raise RuntimeError( 

42 "The starlette.testclient module requires the httpx2 package to be installed.\n" 

43 "You can install this with:\n" 

44 " $ pip install httpx2\n" 

45 ) from None 

46 else: 

47 warnings.warn( 

48 "Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.", 

49 StarletteDeprecationWarning, 

50 stacklevel=2, 

51 ) 

52 

53_PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]] 

54 

55ASGIInstance = Callable[[Receive, Send], Awaitable[None]] 

56ASGI2App = Callable[[Scope], ASGIInstance] 

57ASGI3App = Callable[[Scope, Receive, Send], Awaitable[None]] 

58 

59 

60_RequestData = Mapping[str, str | Iterable[str] | bytes] 

61 

62 

63def _is_asgi3(app: ASGI2App | ASGI3App) -> TypeGuard[ASGI3App]: 

64 if inspect.isclass(app): 

65 return hasattr(app, "__await__") 

66 return is_async_callable(app) 

67 

68 

69class _WrapASGI2: 

70 """ 

71 Provide an ASGI3 interface onto an ASGI2 app. 

72 """ 

73 

74 def __init__(self, app: ASGI2App) -> None: 

75 self.app = app 

76 

77 async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: 

78 instance = self.app(scope) 

79 await instance(receive, send) 

80 

81 

82class _AsyncBackend(TypedDict): 

83 backend: str 

84 backend_options: dict[str, Any] 

85 

86 

87class _Upgrade(Exception): 

88 def __init__(self, session: WebSocketTestSession) -> None: 

89 self.session = session 

90 

91 

92class WebSocketDenialResponse( # type: ignore[misc] 

93 httpx.Response, 

94 WebSocketDisconnect, 

95): 

96 """ 

97 A special case of `WebSocketDisconnect`, raised in the `TestClient` if the 

98 `WebSocket` is closed before being accepted with a `send_denial_response()`. 

99 """ 

100 

101 

102class WebSocketTestSession: 

103 def __init__( 

104 self, 

105 app: ASGI3App, 

106 scope: Scope, 

107 portal_factory: _PortalFactoryType, 

108 ) -> None: 

109 self.app = app 

110 self.scope = scope 

111 self.accepted_subprotocol = None 

112 self.portal_factory = portal_factory 

113 self.extra_headers = None 

114 

115 def __enter__(self) -> Self: 

116 with contextlib.ExitStack() as stack: 

117 self.portal = portal = stack.enter_context(self.portal_factory()) 

118 fut, cs = portal.start_task(self._run) 

119 stack.callback(fut.result) 

120 stack.callback(portal.call, cs.cancel) 

121 self.send({"type": "websocket.connect"}) 

122 message = self.receive() 

123 self._raise_on_close(message) 

124 self.accepted_subprotocol = message.get("subprotocol", None) 

125 self.extra_headers = message.get("headers", None) 

126 stack.callback(self.close, 1000) 

127 self.exit_stack = stack.pop_all() 

128 return self 

129 

130 def __exit__(self, *args: Any) -> bool | None: 

131 return self.exit_stack.__exit__(*args) 

132 

133 async def _run(self, *, task_status: anyio.abc.TaskStatus[anyio.CancelScope]) -> None: 

134 """ 

135 The sub-thread in which the websocket session runs. 

136 """ 

137 send: anyio.create_memory_object_stream[Message] = anyio.create_memory_object_stream(math.inf) 

138 send_tx, send_rx = send 

139 receive: anyio.create_memory_object_stream[Message] = anyio.create_memory_object_stream(math.inf) 

140 receive_tx, receive_rx = receive 

141 with send_tx, send_rx, receive_tx, receive_rx, anyio.CancelScope() as cs: 

142 self._receive_tx = receive_tx 

143 self._send_rx = send_rx 

144 task_status.started(cs) 

145 await self.app(self.scope, receive_rx.receive, send_tx.send) 

146 

147 # wait for cs.cancel to be called before closing streams 

148 await anyio.sleep_forever() 

149 

150 def _raise_on_close(self, message: Message) -> None: 

151 if message["type"] == "websocket.close": 

152 raise WebSocketDisconnect(code=message.get("code", 1000), reason=message.get("reason", "")) 

153 elif message["type"] == "websocket.http.response.start": 

154 status_code: int = message["status"] 

155 headers: list[tuple[bytes, bytes]] = message["headers"] 

156 body: list[bytes] = [] 

157 while True: 

158 message = self.receive() 

159 assert message["type"] == "websocket.http.response.body" 

160 body.append(message["body"]) 

161 if not message.get("more_body", False): 

162 break 

163 raise WebSocketDenialResponse(status_code=status_code, headers=headers, content=b"".join(body)) 

164 

165 def send(self, message: Message) -> None: 

166 self.portal.call(self._receive_tx.send, message) 

167 

168 def send_text(self, data: str) -> None: 

169 self.send({"type": "websocket.receive", "text": data}) 

170 

171 def send_bytes(self, data: bytes) -> None: 

172 self.send({"type": "websocket.receive", "bytes": data}) 

173 

174 def send_json(self, data: Any, mode: Literal["text", "binary"] = "text") -> None: 

175 text = json.dumps(data, separators=(",", ":"), ensure_ascii=False) 

176 if mode == "text": 

177 self.send({"type": "websocket.receive", "text": text}) 

178 else: 

179 self.send({"type": "websocket.receive", "bytes": text.encode("utf-8")}) 

180 

181 def close(self, code: int = 1000, reason: str | None = None) -> None: 

182 self.send({"type": "websocket.disconnect", "code": code, "reason": reason}) 

183 

184 def receive(self) -> Message: 

185 return self.portal.call(self._send_rx.receive) 

186 

187 def receive_text(self) -> str: 

188 message = self.receive() 

189 self._raise_on_close(message) 

190 return cast(str, message["text"]) 

191 

192 def receive_bytes(self) -> bytes: 

193 message = self.receive() 

194 self._raise_on_close(message) 

195 return cast(bytes, message["bytes"]) 

196 

197 def receive_json(self, mode: Literal["text", "binary"] = "text") -> Any: 

198 message = self.receive() 

199 self._raise_on_close(message) 

200 if mode == "text": 

201 text = message["text"] 

202 else: 

203 text = message["bytes"].decode("utf-8") 

204 return json.loads(text) 

205 

206 

207class _TestClientTransport(httpx.BaseTransport): 

208 def __init__( 

209 self, 

210 app: ASGI3App, 

211 portal_factory: _PortalFactoryType, 

212 raise_server_exceptions: bool = True, 

213 root_path: str = "", 

214 *, 

215 client: tuple[str, int], 

216 app_state: dict[str, Any], 

217 ) -> None: 

218 self.app = app 

219 self.raise_server_exceptions = raise_server_exceptions 

220 self.root_path = root_path 

221 self.portal_factory = portal_factory 

222 self.app_state = app_state 

223 self.client = client 

224 

225 def handle_request(self, request: httpx.Request) -> httpx.Response: 

226 scheme = request.url.scheme 

227 netloc = request.url.netloc.decode(encoding="ascii") 

228 path = request.url.path 

229 raw_path = request.url.raw_path 

230 query = request.url.query.decode(encoding="ascii") 

231 

232 default_port = {"http": 80, "ws": 80, "https": 443, "wss": 443}[scheme] 

233 

234 if ":" in netloc: 

235 host, port_string = netloc.split(":", 1) 

236 port = int(port_string) 

237 else: 

238 host = netloc 

239 port = default_port 

240 

241 # Include the 'host' header. 

242 if "host" in request.headers: 

243 headers: list[tuple[bytes, bytes]] = [] 

244 elif port == default_port: # pragma: no cover 

245 headers = [(b"host", host.encode())] 

246 else: # pragma: no cover 

247 headers = [(b"host", (f"{host}:{port}").encode())] 

248 

249 # Include other request headers. 

250 headers += [(key.lower().encode(), value.encode()) for key, value in request.headers.multi_items()] 

251 

252 scope: dict[str, Any] 

253 

254 if scheme in {"ws", "wss"}: 

255 subprotocol = request.headers.get("sec-websocket-protocol", None) 

256 if subprotocol is None: 

257 subprotocols: Sequence[str] = [] 

258 else: 

259 subprotocols = [value.strip() for value in subprotocol.split(",")] 

260 scope = { 

261 "type": "websocket", 

262 "path": unquote(path), 

263 "raw_path": raw_path.split(b"?", 1)[0], 

264 "root_path": self.root_path, 

265 "scheme": scheme, 

266 "query_string": query.encode(), 

267 "headers": headers, 

268 "client": self.client, 

269 "server": [host, port], 

270 "subprotocols": subprotocols, 

271 "state": self.app_state.copy(), 

272 "extensions": {"websocket.http.response": {}}, 

273 } 

274 session = WebSocketTestSession(self.app, scope, self.portal_factory) 

275 raise _Upgrade(session) 

276 

277 scope = { 

278 "type": "http", 

279 "http_version": "1.1", 

280 "method": request.method, 

281 "path": unquote(path), 

282 "raw_path": raw_path.split(b"?", 1)[0], 

283 "root_path": self.root_path, 

284 "scheme": scheme, 

285 "query_string": query.encode(), 

286 "headers": headers, 

287 "client": self.client, 

288 "server": [host, port], 

289 "extensions": {"http.response.debug": {}}, 

290 "state": self.app_state.copy(), 

291 } 

292 

293 request_complete = False 

294 response_started = False 

295 response_complete: anyio.Event 

296 raw_kwargs: dict[str, Any] = {"stream": io.BytesIO()} 

297 template = None 

298 context = None 

299 

300 async def receive() -> Message: 

301 nonlocal request_complete 

302 

303 if request_complete: 

304 if not response_complete.is_set(): 

305 await response_complete.wait() 

306 return {"type": "http.disconnect"} 

307 

308 body = request.read() 

309 if isinstance(body, str): 

310 body_bytes: bytes = body.encode("utf-8") # pragma: no cover 

311 elif body is None: 

312 body_bytes = b"" # pragma: no cover 

313 elif isinstance(body, GeneratorType): 

314 try: # pragma: no cover 

315 chunk = body.send(None) 

316 if isinstance(chunk, str): 

317 chunk = chunk.encode("utf-8") 

318 return {"type": "http.request", "body": chunk, "more_body": True} 

319 except StopIteration: # pragma: no cover 

320 request_complete = True 

321 return {"type": "http.request", "body": b""} 

322 else: 

323 body_bytes = body 

324 

325 request_complete = True 

326 return {"type": "http.request", "body": body_bytes} 

327 

328 async def send(message: Message) -> None: 

329 nonlocal raw_kwargs, response_started, template, context 

330 

331 if message["type"] == "http.response.start": 

332 assert not response_started, 'Received multiple "http.response.start" messages.' 

333 raw_kwargs["status_code"] = message["status"] 

334 raw_kwargs["headers"] = [(key.decode(), value.decode()) for key, value in message.get("headers", [])] 

335 response_started = True 

336 elif message["type"] == "http.response.body": 

337 assert response_started, 'Received "http.response.body" without "http.response.start".' 

338 assert not response_complete.is_set(), 'Received "http.response.body" after response completed.' 

339 body = message.get("body", b"") 

340 more_body = message.get("more_body", False) 

341 if request.method != "HEAD": 

342 raw_kwargs["stream"].write(body) 

343 if not more_body: 

344 raw_kwargs["stream"].seek(0) 

345 response_complete.set() 

346 elif message["type"] == "http.response.debug": 

347 template = message["info"]["template"] 

348 context = message["info"]["context"] 

349 

350 try: 

351 with self.portal_factory() as portal: 

352 response_complete = portal.call(anyio.Event) 

353 portal.call(self.app, scope, receive, send) 

354 except BaseException as exc: 

355 if self.raise_server_exceptions: 

356 raise exc 

357 

358 if self.raise_server_exceptions: 

359 assert response_started, "TestClient did not receive any response." 

360 elif not response_started: 

361 raw_kwargs = { 

362 "status_code": 500, 

363 "headers": [], 

364 "stream": io.BytesIO(), 

365 } 

366 

367 raw_kwargs["stream"] = httpx.ByteStream(raw_kwargs["stream"].read()) 

368 

369 response = httpx.Response(**raw_kwargs, request=request) 

370 if template is not None: 

371 response.template = template # type: ignore[attr-defined] 

372 response.context = context # type: ignore[attr-defined] 

373 return response 

374 

375 

376class TestClient(httpx.Client): 

377 __test__ = False 

378 task: Future[None] 

379 portal: anyio.abc.BlockingPortal | None = None 

380 

381 def __init__( 

382 self, 

383 app: ASGIApp, 

384 base_url: str = "http://testserver", 

385 raise_server_exceptions: bool = True, 

386 root_path: str = "", 

387 backend: Literal["asyncio", "trio"] = "asyncio", 

388 backend_options: dict[str, Any] | None = None, 

389 cookies: httpx._types.CookieTypes | None = None, 

390 headers: dict[str, str] | None = None, 

391 follow_redirects: bool = True, 

392 client: tuple[str, int] = ("testclient", 50000), 

393 ) -> None: 

394 self.async_backend = _AsyncBackend(backend=backend, backend_options=backend_options or {}) 

395 if _is_asgi3(app): 

396 asgi_app = app 

397 else: 

398 app = cast(ASGI2App, app) # type: ignore[assignment] 

399 asgi_app = _WrapASGI2(app) # type: ignore[arg-type] 

400 self.app = asgi_app 

401 self.app_state: dict[str, Any] = {} 

402 transport = _TestClientTransport( 

403 self.app, 

404 portal_factory=self._portal_factory, 

405 raise_server_exceptions=raise_server_exceptions, 

406 root_path=root_path, 

407 app_state=self.app_state, 

408 client=client, 

409 ) 

410 if headers is None: 

411 headers = {} 

412 headers.setdefault("user-agent", "testclient") 

413 super().__init__( 

414 base_url=base_url, 

415 headers=headers, 

416 transport=transport, 

417 follow_redirects=follow_redirects, 

418 cookies=cookies, 

419 ) 

420 

421 @contextlib.contextmanager 

422 def _portal_factory(self) -> Generator[anyio.abc.BlockingPortal, None, None]: 

423 if self.portal is not None: 

424 yield self.portal 

425 else: 

426 with anyio.from_thread.start_blocking_portal(**self.async_backend) as portal: 

427 yield portal 

428 

429 def request( # type: ignore[override] 

430 self, 

431 method: str, 

432 url: httpx._types.URLTypes, 

433 *, 

434 content: httpx._types.RequestContent | None = None, 

435 data: _RequestData | None = None, 

436 files: httpx._types.RequestFiles | None = None, 

437 json: Any = None, 

438 params: httpx._types.QueryParamTypes | None = None, 

439 headers: httpx._types.HeaderTypes | None = None, 

440 cookies: httpx._types.CookieTypes | None = None, 

441 auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

442 follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

443 timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

444 extensions: dict[str, Any] | None = None, 

445 ) -> httpx.Response: 

446 if timeout is not httpx.USE_CLIENT_DEFAULT: 

447 warnings.warn( 

448 "You should not use the 'timeout' argument with the TestClient. " 

449 "See https://github.com/Kludex/starlette/issues/1108 for more information.", 

450 StarletteDeprecationWarning, 

451 stacklevel=2, 

452 ) 

453 url = self._merge_url(url) 

454 return super().request( 

455 method, 

456 url, 

457 content=content, 

458 data=data, 

459 files=files, 

460 json=json, 

461 params=params, 

462 headers=headers, 

463 cookies=cookies, 

464 auth=auth, 

465 follow_redirects=follow_redirects, 

466 timeout=timeout, 

467 extensions=extensions, 

468 ) 

469 

470 def get( # type: ignore[override] 

471 self, 

472 url: httpx._types.URLTypes, 

473 *, 

474 params: httpx._types.QueryParamTypes | None = None, 

475 headers: httpx._types.HeaderTypes | None = None, 

476 cookies: httpx._types.CookieTypes | None = None, 

477 auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

478 follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

479 timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

480 extensions: dict[str, Any] | None = None, 

481 ) -> httpx.Response: 

482 return super().get( 

483 url, 

484 params=params, 

485 headers=headers, 

486 cookies=cookies, 

487 auth=auth, 

488 follow_redirects=follow_redirects, 

489 timeout=timeout, 

490 extensions=extensions, 

491 ) 

492 

493 def options( # type: ignore[override] 

494 self, 

495 url: httpx._types.URLTypes, 

496 *, 

497 params: httpx._types.QueryParamTypes | None = None, 

498 headers: httpx._types.HeaderTypes | None = None, 

499 cookies: httpx._types.CookieTypes | None = None, 

500 auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

501 follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

502 timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

503 extensions: dict[str, Any] | None = None, 

504 ) -> httpx.Response: 

505 return super().options( 

506 url, 

507 params=params, 

508 headers=headers, 

509 cookies=cookies, 

510 auth=auth, 

511 follow_redirects=follow_redirects, 

512 timeout=timeout, 

513 extensions=extensions, 

514 ) 

515 

516 def head( # type: ignore[override] 

517 self, 

518 url: httpx._types.URLTypes, 

519 *, 

520 params: httpx._types.QueryParamTypes | None = None, 

521 headers: httpx._types.HeaderTypes | None = None, 

522 cookies: httpx._types.CookieTypes | None = None, 

523 auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

524 follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

525 timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

526 extensions: dict[str, Any] | None = None, 

527 ) -> httpx.Response: 

528 return super().head( 

529 url, 

530 params=params, 

531 headers=headers, 

532 cookies=cookies, 

533 auth=auth, 

534 follow_redirects=follow_redirects, 

535 timeout=timeout, 

536 extensions=extensions, 

537 ) 

538 

539 def post( # type: ignore[override] 

540 self, 

541 url: httpx._types.URLTypes, 

542 *, 

543 content: httpx._types.RequestContent | None = None, 

544 data: _RequestData | None = None, 

545 files: httpx._types.RequestFiles | None = None, 

546 json: Any = None, 

547 params: httpx._types.QueryParamTypes | None = None, 

548 headers: httpx._types.HeaderTypes | None = None, 

549 cookies: httpx._types.CookieTypes | None = None, 

550 auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

551 follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

552 timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

553 extensions: dict[str, Any] | None = None, 

554 ) -> httpx.Response: 

555 return super().post( 

556 url, 

557 content=content, 

558 data=data, 

559 files=files, 

560 json=json, 

561 params=params, 

562 headers=headers, 

563 cookies=cookies, 

564 auth=auth, 

565 follow_redirects=follow_redirects, 

566 timeout=timeout, 

567 extensions=extensions, 

568 ) 

569 

570 def put( # type: ignore[override] 

571 self, 

572 url: httpx._types.URLTypes, 

573 *, 

574 content: httpx._types.RequestContent | None = None, 

575 data: _RequestData | None = None, 

576 files: httpx._types.RequestFiles | None = None, 

577 json: Any = None, 

578 params: httpx._types.QueryParamTypes | None = None, 

579 headers: httpx._types.HeaderTypes | None = None, 

580 cookies: httpx._types.CookieTypes | None = None, 

581 auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

582 follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

583 timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

584 extensions: dict[str, Any] | None = None, 

585 ) -> httpx.Response: 

586 return super().put( 

587 url, 

588 content=content, 

589 data=data, 

590 files=files, 

591 json=json, 

592 params=params, 

593 headers=headers, 

594 cookies=cookies, 

595 auth=auth, 

596 follow_redirects=follow_redirects, 

597 timeout=timeout, 

598 extensions=extensions, 

599 ) 

600 

601 def patch( # type: ignore[override] 

602 self, 

603 url: httpx._types.URLTypes, 

604 *, 

605 content: httpx._types.RequestContent | None = None, 

606 data: _RequestData | None = None, 

607 files: httpx._types.RequestFiles | None = None, 

608 json: Any = None, 

609 params: httpx._types.QueryParamTypes | None = None, 

610 headers: httpx._types.HeaderTypes | None = None, 

611 cookies: httpx._types.CookieTypes | None = None, 

612 auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

613 follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

614 timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

615 extensions: dict[str, Any] | None = None, 

616 ) -> httpx.Response: 

617 return super().patch( 

618 url, 

619 content=content, 

620 data=data, 

621 files=files, 

622 json=json, 

623 params=params, 

624 headers=headers, 

625 cookies=cookies, 

626 auth=auth, 

627 follow_redirects=follow_redirects, 

628 timeout=timeout, 

629 extensions=extensions, 

630 ) 

631 

632 def delete( # type: ignore[override] 

633 self, 

634 url: httpx._types.URLTypes, 

635 *, 

636 params: httpx._types.QueryParamTypes | None = None, 

637 headers: httpx._types.HeaderTypes | None = None, 

638 cookies: httpx._types.CookieTypes | None = None, 

639 auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

640 follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

641 timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, 

642 extensions: dict[str, Any] | None = None, 

643 ) -> httpx.Response: 

644 return super().delete( 

645 url, 

646 params=params, 

647 headers=headers, 

648 cookies=cookies, 

649 auth=auth, 

650 follow_redirects=follow_redirects, 

651 timeout=timeout, 

652 extensions=extensions, 

653 ) 

654 

655 def websocket_connect( 

656 self, 

657 url: str, 

658 subprotocols: Sequence[str] | None = None, 

659 **kwargs: Any, 

660 ) -> WebSocketTestSession: 

661 url = urljoin("ws://testserver", url) 

662 headers = kwargs.get("headers", {}) 

663 headers.setdefault("connection", "upgrade") 

664 headers.setdefault("sec-websocket-key", "testserver==") 

665 headers.setdefault("sec-websocket-version", "13") 

666 if subprotocols is not None: 

667 headers.setdefault("sec-websocket-protocol", ", ".join(subprotocols)) 

668 kwargs["headers"] = headers 

669 try: 

670 super().request("GET", url, **kwargs) 

671 except _Upgrade as exc: 

672 session = exc.session 

673 else: 

674 raise RuntimeError("Expected WebSocket upgrade") # pragma: no cover 

675 

676 return session 

677 

678 def __enter__(self) -> Self: 

679 with contextlib.ExitStack() as stack: 

680 self.portal = portal = stack.enter_context(anyio.from_thread.start_blocking_portal(**self.async_backend)) 

681 

682 @stack.callback 

683 def reset_portal() -> None: 

684 self.portal = None 

685 

686 send: anyio.create_memory_object_stream[MutableMapping[str, Any] | None] = ( 

687 anyio.create_memory_object_stream(math.inf) 

688 ) 

689 receive: anyio.create_memory_object_stream[MutableMapping[str, Any]] = anyio.create_memory_object_stream( 

690 math.inf 

691 ) 

692 for channel in (*send, *receive): 

693 stack.callback(channel.close) 

694 self.stream_send = StapledObjectStream(*send) 

695 self.stream_receive = StapledObjectStream(*receive) 

696 self.task = portal.start_task_soon(self.lifespan) 

697 portal.call(self.wait_startup) 

698 

699 @stack.callback 

700 def wait_shutdown() -> None: 

701 portal.call(self.wait_shutdown) 

702 

703 self.exit_stack = stack.pop_all() 

704 

705 return self 

706 

707 def __exit__(self, *args: Any) -> None: 

708 self.exit_stack.close() 

709 

710 async def lifespan(self) -> None: 

711 scope = {"type": "lifespan", "state": self.app_state} 

712 try: 

713 await self.app(scope, self.stream_receive.receive, self.stream_send.send) 

714 finally: 

715 await self.stream_send.send(None) 

716 

717 async def wait_startup(self) -> None: 

718 await self.stream_receive.send({"type": "lifespan.startup"}) 

719 

720 async def receive() -> Any: 

721 message = await self.stream_send.receive() 

722 if message is None: 

723 self.task.result() 

724 return message 

725 

726 message = await receive() 

727 assert message["type"] in ( 

728 "lifespan.startup.complete", 

729 "lifespan.startup.failed", 

730 ) 

731 if message["type"] == "lifespan.startup.failed": 

732 await receive() 

733 

734 async def wait_shutdown(self) -> None: 

735 async def receive() -> Any: 

736 message = await self.stream_send.receive() 

737 if message is None: 

738 self.task.result() 

739 return message 

740 

741 await self.stream_receive.send({"type": "lifespan.shutdown"}) 

742 message = await receive() 

743 assert message["type"] in ( 

744 "lifespan.shutdown.complete", 

745 "lifespan.shutdown.failed", 

746 ) 

747 if message["type"] == "lifespan.shutdown.failed": 

748 await receive()