datastructures.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. import typing
  2. from shlex import shlex
  3. from urllib.parse import SplitResult, parse_qsl, urlencode, urlsplit
  4. from starlette.concurrency import run_in_threadpool
  5. from starlette.types import Scope
  6. class Address(typing.NamedTuple):
  7. host: str
  8. port: int
  9. _KeyType = typing.TypeVar("_KeyType")
  10. # Mapping keys are invariant but their values are covariant since
  11. # you can only read them
  12. # that is, you can't do `Mapping[str, Animal]()["fido"] = Dog()`
  13. _CovariantValueType = typing.TypeVar("_CovariantValueType", covariant=True)
  14. class URL:
  15. def __init__(
  16. self,
  17. url: str = "",
  18. scope: typing.Optional[Scope] = None,
  19. **components: typing.Any,
  20. ) -> None:
  21. if scope is not None:
  22. assert not url, 'Cannot set both "url" and "scope".'
  23. assert not components, 'Cannot set both "scope" and "**components".'
  24. scheme = scope.get("scheme", "http")
  25. server = scope.get("server", None)
  26. path = scope["path"]
  27. query_string = scope.get("query_string", b"")
  28. host_header = None
  29. for key, value in scope["headers"]:
  30. if key == b"host":
  31. host_header = value.decode("latin-1")
  32. break
  33. if host_header is not None:
  34. url = f"{scheme}://{host_header}{path}"
  35. elif server is None:
  36. url = path
  37. else:
  38. host, port = server
  39. default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme]
  40. if port == default_port:
  41. url = f"{scheme}://{host}{path}"
  42. else:
  43. url = f"{scheme}://{host}:{port}{path}"
  44. if query_string:
  45. url += "?" + query_string.decode()
  46. elif components:
  47. assert not url, 'Cannot set both "url" and "**components".'
  48. url = URL("").replace(**components).components.geturl()
  49. self._url = url
  50. @property
  51. def components(self) -> SplitResult:
  52. if not hasattr(self, "_components"):
  53. self._components = urlsplit(self._url)
  54. return self._components
  55. @property
  56. def scheme(self) -> str:
  57. return self.components.scheme
  58. @property
  59. def netloc(self) -> str:
  60. return self.components.netloc
  61. @property
  62. def path(self) -> str:
  63. return self.components.path
  64. @property
  65. def query(self) -> str:
  66. return self.components.query
  67. @property
  68. def fragment(self) -> str:
  69. return self.components.fragment
  70. @property
  71. def username(self) -> typing.Union[None, str]:
  72. return self.components.username
  73. @property
  74. def password(self) -> typing.Union[None, str]:
  75. return self.components.password
  76. @property
  77. def hostname(self) -> typing.Union[None, str]:
  78. return self.components.hostname
  79. @property
  80. def port(self) -> typing.Optional[int]:
  81. return self.components.port
  82. @property
  83. def is_secure(self) -> bool:
  84. return self.scheme in ("https", "wss")
  85. def replace(self, **kwargs: typing.Any) -> "URL":
  86. if (
  87. "username" in kwargs
  88. or "password" in kwargs
  89. or "hostname" in kwargs
  90. or "port" in kwargs
  91. ):
  92. hostname = kwargs.pop("hostname", None)
  93. port = kwargs.pop("port", self.port)
  94. username = kwargs.pop("username", self.username)
  95. password = kwargs.pop("password", self.password)
  96. if hostname is None:
  97. netloc = self.netloc
  98. _, _, hostname = netloc.rpartition("@")
  99. if hostname[-1] != "]":
  100. hostname = hostname.rsplit(":", 1)[0]
  101. netloc = hostname
  102. if port is not None:
  103. netloc += f":{port}"
  104. if username is not None:
  105. userpass = username
  106. if password is not None:
  107. userpass += f":{password}"
  108. netloc = f"{userpass}@{netloc}"
  109. kwargs["netloc"] = netloc
  110. components = self.components._replace(**kwargs)
  111. return self.__class__(components.geturl())
  112. def include_query_params(self, **kwargs: typing.Any) -> "URL":
  113. params = MultiDict(parse_qsl(self.query, keep_blank_values=True))
  114. params.update({str(key): str(value) for key, value in kwargs.items()})
  115. query = urlencode(params.multi_items())
  116. return self.replace(query=query)
  117. def replace_query_params(self, **kwargs: typing.Any) -> "URL":
  118. query = urlencode([(str(key), str(value)) for key, value in kwargs.items()])
  119. return self.replace(query=query)
  120. def remove_query_params(
  121. self, keys: typing.Union[str, typing.Sequence[str]]
  122. ) -> "URL":
  123. if isinstance(keys, str):
  124. keys = [keys]
  125. params = MultiDict(parse_qsl(self.query, keep_blank_values=True))
  126. for key in keys:
  127. params.pop(key, None)
  128. query = urlencode(params.multi_items())
  129. return self.replace(query=query)
  130. def __eq__(self, other: typing.Any) -> bool:
  131. return str(self) == str(other)
  132. def __str__(self) -> str:
  133. return self._url
  134. def __repr__(self) -> str:
  135. url = str(self)
  136. if self.password:
  137. url = str(self.replace(password="********"))
  138. return f"{self.__class__.__name__}({repr(url)})"
  139. class URLPath(str):
  140. """
  141. A URL path string that may also hold an associated protocol and/or host.
  142. Used by the routing to return `url_path_for` matches.
  143. """
  144. def __new__(cls, path: str, protocol: str = "", host: str = "") -> "URLPath":
  145. assert protocol in ("http", "websocket", "")
  146. return str.__new__(cls, path)
  147. def __init__(self, path: str, protocol: str = "", host: str = "") -> None:
  148. self.protocol = protocol
  149. self.host = host
  150. def make_absolute_url(self, base_url: typing.Union[str, URL]) -> URL:
  151. if isinstance(base_url, str):
  152. base_url = URL(base_url)
  153. if self.protocol:
  154. scheme = {
  155. "http": {True: "https", False: "http"},
  156. "websocket": {True: "wss", False: "ws"},
  157. }[self.protocol][base_url.is_secure]
  158. else:
  159. scheme = base_url.scheme
  160. netloc = self.host or base_url.netloc
  161. path = base_url.path.rstrip("/") + str(self)
  162. return URL(scheme=scheme, netloc=netloc, path=path)
  163. class Secret:
  164. """
  165. Holds a string value that should not be revealed in tracebacks etc.
  166. You should cast the value to `str` at the point it is required.
  167. """
  168. def __init__(self, value: str):
  169. self._value = value
  170. def __repr__(self) -> str:
  171. class_name = self.__class__.__name__
  172. return f"{class_name}('**********')"
  173. def __str__(self) -> str:
  174. return self._value
  175. def __bool__(self) -> bool:
  176. return bool(self._value)
  177. class CommaSeparatedStrings(typing.Sequence[str]):
  178. def __init__(self, value: typing.Union[str, typing.Sequence[str]]):
  179. if isinstance(value, str):
  180. splitter = shlex(value, posix=True)
  181. splitter.whitespace = ","
  182. splitter.whitespace_split = True
  183. self._items = [item.strip() for item in splitter]
  184. else:
  185. self._items = list(value)
  186. def __len__(self) -> int:
  187. return len(self._items)
  188. def __getitem__(self, index: typing.Union[int, slice]) -> typing.Any:
  189. return self._items[index]
  190. def __iter__(self) -> typing.Iterator[str]:
  191. return iter(self._items)
  192. def __repr__(self) -> str:
  193. class_name = self.__class__.__name__
  194. items = [item for item in self]
  195. return f"{class_name}({items!r})"
  196. def __str__(self) -> str:
  197. return ", ".join(repr(item) for item in self)
  198. class ImmutableMultiDict(typing.Mapping[_KeyType, _CovariantValueType]):
  199. _dict: typing.Dict[_KeyType, _CovariantValueType]
  200. def __init__(
  201. self,
  202. *args: typing.Union[
  203. "ImmutableMultiDict[_KeyType, _CovariantValueType]",
  204. typing.Mapping[_KeyType, _CovariantValueType],
  205. typing.Iterable[typing.Tuple[_KeyType, _CovariantValueType]],
  206. ],
  207. **kwargs: typing.Any,
  208. ) -> None:
  209. assert len(args) < 2, "Too many arguments."
  210. value: typing.Any = args[0] if args else []
  211. if kwargs:
  212. value = (
  213. ImmutableMultiDict(value).multi_items()
  214. + ImmutableMultiDict(kwargs).multi_items()
  215. )
  216. if not value:
  217. _items: typing.List[typing.Tuple[typing.Any, typing.Any]] = []
  218. elif hasattr(value, "multi_items"):
  219. value = typing.cast(
  220. ImmutableMultiDict[_KeyType, _CovariantValueType], value
  221. )
  222. _items = list(value.multi_items())
  223. elif hasattr(value, "items"):
  224. value = typing.cast(typing.Mapping[_KeyType, _CovariantValueType], value)
  225. _items = list(value.items())
  226. else:
  227. value = typing.cast(
  228. typing.List[typing.Tuple[typing.Any, typing.Any]], value
  229. )
  230. _items = list(value)
  231. self._dict = {k: v for k, v in _items}
  232. self._list = _items
  233. def getlist(self, key: typing.Any) -> typing.List[_CovariantValueType]:
  234. return [item_value for item_key, item_value in self._list if item_key == key]
  235. def keys(self) -> typing.KeysView[_KeyType]:
  236. return self._dict.keys()
  237. def values(self) -> typing.ValuesView[_CovariantValueType]:
  238. return self._dict.values()
  239. def items(self) -> typing.ItemsView[_KeyType, _CovariantValueType]:
  240. return self._dict.items()
  241. def multi_items(self) -> typing.List[typing.Tuple[_KeyType, _CovariantValueType]]:
  242. return list(self._list)
  243. def __getitem__(self, key: _KeyType) -> _CovariantValueType:
  244. return self._dict[key]
  245. def __contains__(self, key: typing.Any) -> bool:
  246. return key in self._dict
  247. def __iter__(self) -> typing.Iterator[_KeyType]:
  248. return iter(self.keys())
  249. def __len__(self) -> int:
  250. return len(self._dict)
  251. def __eq__(self, other: typing.Any) -> bool:
  252. if not isinstance(other, self.__class__):
  253. return False
  254. return sorted(self._list) == sorted(other._list)
  255. def __repr__(self) -> str:
  256. class_name = self.__class__.__name__
  257. items = self.multi_items()
  258. return f"{class_name}({items!r})"
  259. class MultiDict(ImmutableMultiDict[typing.Any, typing.Any]):
  260. def __setitem__(self, key: typing.Any, value: typing.Any) -> None:
  261. self.setlist(key, [value])
  262. def __delitem__(self, key: typing.Any) -> None:
  263. self._list = [(k, v) for k, v in self._list if k != key]
  264. del self._dict[key]
  265. def pop(self, key: typing.Any, default: typing.Any = None) -> typing.Any:
  266. self._list = [(k, v) for k, v in self._list if k != key]
  267. return self._dict.pop(key, default)
  268. def popitem(self) -> typing.Tuple[typing.Any, typing.Any]:
  269. key, value = self._dict.popitem()
  270. self._list = [(k, v) for k, v in self._list if k != key]
  271. return key, value
  272. def poplist(self, key: typing.Any) -> typing.List[typing.Any]:
  273. values = [v for k, v in self._list if k == key]
  274. self.pop(key)
  275. return values
  276. def clear(self) -> None:
  277. self._dict.clear()
  278. self._list.clear()
  279. def setdefault(self, key: typing.Any, default: typing.Any = None) -> typing.Any:
  280. if key not in self:
  281. self._dict[key] = default
  282. self._list.append((key, default))
  283. return self[key]
  284. def setlist(self, key: typing.Any, values: typing.List[typing.Any]) -> None:
  285. if not values:
  286. self.pop(key, None)
  287. else:
  288. existing_items = [(k, v) for (k, v) in self._list if k != key]
  289. self._list = existing_items + [(key, value) for value in values]
  290. self._dict[key] = values[-1]
  291. def append(self, key: typing.Any, value: typing.Any) -> None:
  292. self._list.append((key, value))
  293. self._dict[key] = value
  294. def update(
  295. self,
  296. *args: typing.Union[
  297. "MultiDict",
  298. typing.Mapping[typing.Any, typing.Any],
  299. typing.List[typing.Tuple[typing.Any, typing.Any]],
  300. ],
  301. **kwargs: typing.Any,
  302. ) -> None:
  303. value = MultiDict(*args, **kwargs)
  304. existing_items = [(k, v) for (k, v) in self._list if k not in value.keys()]
  305. self._list = existing_items + value.multi_items()
  306. self._dict.update(value)
  307. class QueryParams(ImmutableMultiDict[str, str]):
  308. """
  309. An immutable multidict.
  310. """
  311. def __init__(
  312. self,
  313. *args: typing.Union[
  314. "ImmutableMultiDict[typing.Any, typing.Any]",
  315. typing.Mapping[typing.Any, typing.Any],
  316. typing.List[typing.Tuple[typing.Any, typing.Any]],
  317. str,
  318. bytes,
  319. ],
  320. **kwargs: typing.Any,
  321. ) -> None:
  322. assert len(args) < 2, "Too many arguments."
  323. value = args[0] if args else []
  324. if isinstance(value, str):
  325. super().__init__(parse_qsl(value, keep_blank_values=True), **kwargs)
  326. elif isinstance(value, bytes):
  327. super().__init__(
  328. parse_qsl(value.decode("latin-1"), keep_blank_values=True), **kwargs
  329. )
  330. else:
  331. super().__init__(*args, **kwargs) # type: ignore[arg-type]
  332. self._list = [(str(k), str(v)) for k, v in self._list]
  333. self._dict = {str(k): str(v) for k, v in self._dict.items()}
  334. def __str__(self) -> str:
  335. return urlencode(self._list)
  336. def __repr__(self) -> str:
  337. class_name = self.__class__.__name__
  338. query_string = str(self)
  339. return f"{class_name}({query_string!r})"
  340. class UploadFile:
  341. """
  342. An uploaded file included as part of the request data.
  343. """
  344. def __init__(
  345. self,
  346. file: typing.BinaryIO,
  347. *,
  348. size: typing.Optional[int] = None,
  349. filename: typing.Optional[str] = None,
  350. headers: "typing.Optional[Headers]" = None,
  351. ) -> None:
  352. self.filename = filename
  353. self.file = file
  354. self.size = size
  355. self.headers = headers or Headers()
  356. @property
  357. def content_type(self) -> typing.Optional[str]:
  358. return self.headers.get("content-type", None)
  359. @property
  360. def _in_memory(self) -> bool:
  361. # check for SpooledTemporaryFile._rolled
  362. rolled_to_disk = getattr(self.file, "_rolled", True)
  363. return not rolled_to_disk
  364. async def write(self, data: bytes) -> None:
  365. if self.size is not None:
  366. self.size += len(data)
  367. if self._in_memory:
  368. self.file.write(data)
  369. else:
  370. await run_in_threadpool(self.file.write, data)
  371. async def read(self, size: int = -1) -> bytes:
  372. if self._in_memory:
  373. return self.file.read(size)
  374. return await run_in_threadpool(self.file.read, size)
  375. async def seek(self, offset: int) -> None:
  376. if self._in_memory:
  377. self.file.seek(offset)
  378. else:
  379. await run_in_threadpool(self.file.seek, offset)
  380. async def close(self) -> None:
  381. if self._in_memory:
  382. self.file.close()
  383. else:
  384. await run_in_threadpool(self.file.close)
  385. def __repr__(self) -> str:
  386. return (
  387. f"{self.__class__.__name__}("
  388. f"filename={self.filename!r}, "
  389. f"size={self.size!r}, "
  390. f"headers={self.headers!r})"
  391. )
  392. class FormData(ImmutableMultiDict[str, typing.Union[UploadFile, str]]):
  393. """
  394. An immutable multidict, containing both file uploads and text input.
  395. """
  396. def __init__(
  397. self,
  398. *args: typing.Union[
  399. "FormData",
  400. typing.Mapping[str, typing.Union[str, UploadFile]],
  401. typing.List[typing.Tuple[str, typing.Union[str, UploadFile]]],
  402. ],
  403. **kwargs: typing.Union[str, UploadFile],
  404. ) -> None:
  405. super().__init__(*args, **kwargs)
  406. async def close(self) -> None:
  407. for key, value in self.multi_items():
  408. if isinstance(value, UploadFile):
  409. await value.close()
  410. class Headers(typing.Mapping[str, str]):
  411. """
  412. An immutable, case-insensitive multidict.
  413. """
  414. def __init__(
  415. self,
  416. headers: typing.Optional[typing.Mapping[str, str]] = None,
  417. raw: typing.Optional[typing.List[typing.Tuple[bytes, bytes]]] = None,
  418. scope: typing.Optional[typing.MutableMapping[str, typing.Any]] = None,
  419. ) -> None:
  420. self._list: typing.List[typing.Tuple[bytes, bytes]] = []
  421. if headers is not None:
  422. assert raw is None, 'Cannot set both "headers" and "raw".'
  423. assert scope is None, 'Cannot set both "headers" and "scope".'
  424. self._list = [
  425. (key.lower().encode("latin-1"), value.encode("latin-1"))
  426. for key, value in headers.items()
  427. ]
  428. elif raw is not None:
  429. assert scope is None, 'Cannot set both "raw" and "scope".'
  430. self._list = raw
  431. elif scope is not None:
  432. # scope["headers"] isn't necessarily a list
  433. # it might be a tuple or other iterable
  434. self._list = scope["headers"] = list(scope["headers"])
  435. @property
  436. def raw(self) -> typing.List[typing.Tuple[bytes, bytes]]:
  437. return list(self._list)
  438. def keys(self) -> typing.List[str]: # type: ignore[override]
  439. return [key.decode("latin-1") for key, value in self._list]
  440. def values(self) -> typing.List[str]: # type: ignore[override]
  441. return [value.decode("latin-1") for key, value in self._list]
  442. def items(self) -> typing.List[typing.Tuple[str, str]]: # type: ignore[override]
  443. return [
  444. (key.decode("latin-1"), value.decode("latin-1"))
  445. for key, value in self._list
  446. ]
  447. def getlist(self, key: str) -> typing.List[str]:
  448. get_header_key = key.lower().encode("latin-1")
  449. return [
  450. item_value.decode("latin-1")
  451. for item_key, item_value in self._list
  452. if item_key == get_header_key
  453. ]
  454. def mutablecopy(self) -> "MutableHeaders":
  455. return MutableHeaders(raw=self._list[:])
  456. def __getitem__(self, key: str) -> str:
  457. get_header_key = key.lower().encode("latin-1")
  458. for header_key, header_value in self._list:
  459. if header_key == get_header_key:
  460. return header_value.decode("latin-1")
  461. raise KeyError(key)
  462. def __contains__(self, key: typing.Any) -> bool:
  463. get_header_key = key.lower().encode("latin-1")
  464. for header_key, header_value in self._list:
  465. if header_key == get_header_key:
  466. return True
  467. return False
  468. def __iter__(self) -> typing.Iterator[typing.Any]:
  469. return iter(self.keys())
  470. def __len__(self) -> int:
  471. return len(self._list)
  472. def __eq__(self, other: typing.Any) -> bool:
  473. if not isinstance(other, Headers):
  474. return False
  475. return sorted(self._list) == sorted(other._list)
  476. def __repr__(self) -> str:
  477. class_name = self.__class__.__name__
  478. as_dict = dict(self.items())
  479. if len(as_dict) == len(self):
  480. return f"{class_name}({as_dict!r})"
  481. return f"{class_name}(raw={self.raw!r})"
  482. class MutableHeaders(Headers):
  483. def __setitem__(self, key: str, value: str) -> None:
  484. """
  485. Set the header `key` to `value`, removing any duplicate entries.
  486. Retains insertion order.
  487. """
  488. set_key = key.lower().encode("latin-1")
  489. set_value = value.encode("latin-1")
  490. found_indexes: "typing.List[int]" = []
  491. for idx, (item_key, item_value) in enumerate(self._list):
  492. if item_key == set_key:
  493. found_indexes.append(idx)
  494. for idx in reversed(found_indexes[1:]):
  495. del self._list[idx]
  496. if found_indexes:
  497. idx = found_indexes[0]
  498. self._list[idx] = (set_key, set_value)
  499. else:
  500. self._list.append((set_key, set_value))
  501. def __delitem__(self, key: str) -> None:
  502. """
  503. Remove the header `key`.
  504. """
  505. del_key = key.lower().encode("latin-1")
  506. pop_indexes: "typing.List[int]" = []
  507. for idx, (item_key, item_value) in enumerate(self._list):
  508. if item_key == del_key:
  509. pop_indexes.append(idx)
  510. for idx in reversed(pop_indexes):
  511. del self._list[idx]
  512. def __ior__(self, other: typing.Mapping[str, str]) -> "MutableHeaders":
  513. if not isinstance(other, typing.Mapping):
  514. raise TypeError(f"Expected a mapping but got {other.__class__.__name__}")
  515. self.update(other)
  516. return self
  517. def __or__(self, other: typing.Mapping[str, str]) -> "MutableHeaders":
  518. if not isinstance(other, typing.Mapping):
  519. raise TypeError(f"Expected a mapping but got {other.__class__.__name__}")
  520. new = self.mutablecopy()
  521. new.update(other)
  522. return new
  523. @property
  524. def raw(self) -> typing.List[typing.Tuple[bytes, bytes]]:
  525. return self._list
  526. def setdefault(self, key: str, value: str) -> str:
  527. """
  528. If the header `key` does not exist, then set it to `value`.
  529. Returns the header value.
  530. """
  531. set_key = key.lower().encode("latin-1")
  532. set_value = value.encode("latin-1")
  533. for idx, (item_key, item_value) in enumerate(self._list):
  534. if item_key == set_key:
  535. return item_value.decode("latin-1")
  536. self._list.append((set_key, set_value))
  537. return value
  538. def update(self, other: typing.Mapping[str, str]) -> None:
  539. for key, val in other.items():
  540. self[key] = val
  541. def append(self, key: str, value: str) -> None:
  542. """
  543. Append a header, preserving any duplicate entries.
  544. """
  545. append_key = key.lower().encode("latin-1")
  546. append_value = value.encode("latin-1")
  547. self._list.append((append_key, append_value))
  548. def add_vary_header(self, vary: str) -> None:
  549. existing = self.get("vary")
  550. if existing is not None:
  551. vary = ", ".join([existing, vary])
  552. self["vary"] = vary
  553. class State:
  554. """
  555. An object that can be used to store arbitrary state.
  556. Used for `request.state` and `app.state`.
  557. """
  558. _state: typing.Dict[str, typing.Any]
  559. def __init__(self, state: typing.Optional[typing.Dict[str, typing.Any]] = None):
  560. if state is None:
  561. state = {}
  562. super().__setattr__("_state", state)
  563. def __setattr__(self, key: typing.Any, value: typing.Any) -> None:
  564. self._state[key] = value
  565. def __getattr__(self, key: typing.Any) -> typing.Any:
  566. try:
  567. return self._state[key]
  568. except KeyError:
  569. message = "'{}' object has no attribute '{}'"
  570. raise AttributeError(message.format(self.__class__.__name__, key))
  571. def __delattr__(self, key: typing.Any) -> None:
  572. del self._state[key]