| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700 |
- """
- h2/utilities
- ~~~~~~~~~~~~
- Utility functions that do not belong in a separate module.
- """
- from __future__ import annotations
- import collections
- from typing import TYPE_CHECKING, Any, NamedTuple
- from hpack.struct import HeaderTuple, NeverIndexedHeaderTuple
- from .exceptions import FlowControlError, ProtocolError
- if TYPE_CHECKING: # pragma: no cover
- from collections.abc import Generator, Iterable
- from hpack.struct import Header, HeaderWeaklyTyped
- SIGIL = ord(b":")
- INFORMATIONAL_START = ord(b"1")
- # A set of headers that are hop-by-hop or connection-specific and thus
- # forbidden in HTTP/2. This list comes from RFC 7540 § 8.1.2.2.
- CONNECTION_HEADERS = frozenset([
- b"connection",
- b"proxy-connection",
- b"keep-alive",
- b"transfer-encoding",
- b"upgrade",
- ])
- _ALLOWED_PSEUDO_HEADER_FIELDS = frozenset([
- b":method",
- b":scheme",
- b":authority",
- b":path",
- b":status",
- b":protocol",
- ])
- _SECURE_HEADERS = frozenset([
- # May have basic credentials which are vulnerable to dictionary attacks.
- b"authorization",
- b"proxy-authorization",
- ])
- _REQUEST_ONLY_HEADERS = frozenset([
- b":scheme",
- b":path",
- b":authority",
- b":method",
- b":protocol",
- ])
- _RESPONSE_ONLY_HEADERS = frozenset([b":status"])
- # A Set of pseudo headers that are only valid if the method is
- # CONNECT, see RFC 8441 § 5
- _CONNECT_REQUEST_ONLY_HEADERS = frozenset([b":protocol"])
- def _secure_headers(headers: Iterable[Header],
- hdr_validation_flags: HeaderValidationFlags | None) -> Generator[Header, None, None]:
- """
- Certain headers are at risk of being attacked during the header compression
- phase, and so need to be kept out of header compression contexts. This
- function automatically transforms certain specific headers into HPACK
- never-indexed fields to ensure they don't get added to header compression
- contexts.
- This function currently implements two rules:
- - 'authorization' and 'proxy-authorization' fields are automatically made
- never-indexed.
- - Any 'cookie' header field shorter than 20 bytes long is made
- never-indexed.
- These fields are the most at-risk. These rules are inspired by Firefox
- and nghttp2.
- """
- for header in headers:
- assert isinstance(header[0], bytes)
- if header[0] in _SECURE_HEADERS or (header[0] in b"cookie" and len(header[1]) < 20):
- yield NeverIndexedHeaderTuple(header[0], header[1])
- else:
- yield header
- def extract_method_header(headers: Iterable[Header]) -> bytes | None:
- """
- Extracts the request method from the headers list.
- """
- for k, v in headers:
- if isinstance(v, bytes) and k == b":method":
- return v
- if isinstance(v, str) and k == ":method":
- return v.encode("utf-8") # pragma: no cover
- return None
- def is_informational_response(headers: Iterable[Header]) -> bool:
- """
- Searches headers list for a :status header to confirm that a given
- collection of headers are an informational response. Assumes the header
- are well formed and encoded as bytes: that is, that the HTTP/2 special
- headers are first in the block, and so that it can stop looking when it
- finds the first header field whose name does not begin with a colon.
- :param headers: The HTTP/2 headers.
- :returns: A boolean indicating if this is an informational response.
- """
- for n, v in headers:
- if not n.startswith(b":"):
- return False
- if n != b":status":
- # If we find a non-special header, we're done here: stop looping.
- continue
- # If the first digit is a 1, we've got informational headers.
- return v.startswith(b"1")
- return False
- def guard_increment_window(current: int, increment: int) -> int:
- """
- Increments a flow control window, guarding against that window becoming too
- large.
- :param current: The current value of the flow control window.
- :param increment: The increment to apply to that window.
- :returns: The new value of the window.
- :raises: ``FlowControlError``
- """
- # The largest value the flow control window may take.
- LARGEST_FLOW_CONTROL_WINDOW = 2**31 - 1 # noqa: N806
- new_size = current + increment
- if new_size > LARGEST_FLOW_CONTROL_WINDOW:
- msg = f"May not increment flow control window past {LARGEST_FLOW_CONTROL_WINDOW}"
- raise FlowControlError(msg)
- return new_size
- def authority_from_headers(headers: Iterable[Header]) -> bytes | None:
- """
- Given a header set, searches for the authority header and returns the
- value.
- Note that this doesn't use indexing, so should only be called if the
- headers are for a client request. Otherwise, will loop over the entire
- header set, which is potentially unwise.
- :param headers: The HTTP header set.
- :returns: The value of the authority header, or ``None``.
- :rtype: ``bytes`` or ``None``.
- """
- for n, v in headers:
- if n == b":authority":
- return v
- return None
- # Flags used by the validate_headers pipeline to determine which checks
- # should be applied to a given set of headers.
- class HeaderValidationFlags(NamedTuple):
- is_client: bool
- is_trailer: bool
- is_response_header: bool
- is_push_promise: bool
- def validate_headers(headers: Iterable[Header], hdr_validation_flags: HeaderValidationFlags) -> Iterable[Header]:
- """
- Validates a header sequence against a set of constraints from RFC 7540.
- :param headers: The HTTP header set.
- :param hdr_validation_flags: An instance of HeaderValidationFlags.
- """
- # This validation logic is built on a sequence of generators that are
- # iterated over to provide the final header list. This reduces some of the
- # overhead of doing this checking. However, it's worth noting that this
- # checking remains somewhat expensive, and attempts should be made wherever
- # possible to reduce the time spent doing them.
- #
- # For example, we avoid tuple unpacking in loops because it represents a
- # fixed cost that we don't want to spend, instead indexing into the header
- # tuples.
- headers = _reject_illegal_characters(
- headers, hdr_validation_flags,
- )
- headers = _reject_empty_header_names(
- headers, hdr_validation_flags,
- )
- headers = _reject_te(
- headers, hdr_validation_flags,
- )
- headers = _reject_connection_header(
- headers, hdr_validation_flags,
- )
- headers = _reject_pseudo_header_fields(
- headers, hdr_validation_flags,
- )
- headers = _check_host_authority_header(
- headers, hdr_validation_flags,
- )
- return _check_path_header(headers, hdr_validation_flags)
- def _reject_illegal_characters(headers: Iterable[Header],
- hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
- """
- Raises a ProtocolError if any header names or values contain illegal characters.
- See <https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.1>.
- """
- for header in headers:
- # > A field name MUST NOT contain characters in the ranges 0x00-0x20, 0x41-0x5a,
- # > or 0x7f-0xff (all ranges inclusive).
- for c in header[0]:
- if 0x41 <= c <= 0x5a:
- msg = f"Received uppercase header name {header[0]!r}."
- raise ProtocolError(msg)
- if c <= 0x20 or c >= 0x7f:
- msg = f"Illegal character '{chr(c)}' in header name: {header[0]!r}"
- raise ProtocolError(msg)
- # > With the exception of pseudo-header fields (Section 8.3), which have a name
- # > that starts with a single colon, field names MUST NOT include a colon (ASCII
- # > COLON, 0x3a).
- if header[0].find(b":", 1) != -1:
- msg = f"Illegal character ':' in header name: {header[0]!r}"
- raise ProtocolError(msg)
- # For compatibility with RFC 7230 header fields, we need to allow the field
- # value to be an empty string. This is ludicrous, but technically allowed.
- if field_value := header[1]:
- # > A field value MUST NOT contain the zero value (ASCII NUL, 0x00), line feed
- # > (ASCII LF, 0x0a), or carriage return (ASCII CR, 0x0d) at any position.
- for c in field_value:
- if c == 0 or c == 0x0a or c == 0x0d: # noqa: PLR1714
- msg = f"Illegal character '{chr(c)}' in header value: {field_value!r}"
- raise ProtocolError(msg)
- # > A field value MUST NOT start or end with an ASCII whitespace character
- # > (ASCII SP or HTAB, 0x20 or 0x09).
- if (
- field_value[0] == 0x20 or
- field_value[0] == 0x09 or
- field_value[-1] == 0x20 or
- field_value[-1] == 0x09
- ):
- msg = f"Received header value surrounded by whitespace {field_value!r}"
- raise ProtocolError(msg)
- yield header
- def _reject_empty_header_names(headers: Iterable[Header],
- hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
- """
- Raises a ProtocolError if any header names are empty (length 0).
- While hpack decodes such headers without errors, they are semantically
- forbidden in HTTP, see RFC 7230, stating that they must be at least one
- character long.
- """
- for header in headers:
- if len(header[0]) == 0:
- msg = "Received header name with zero length."
- raise ProtocolError(msg)
- yield header
- def _reject_te(headers: Iterable[Header], hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
- """
- Raises a ProtocolError if the TE header is present in a header block and
- its value is anything other than "trailers".
- """
- for header in headers:
- if header[0] == b"te" and header[1].lower() != b"trailers":
- msg = f"Invalid value for TE header: {header[1]!r}"
- raise ProtocolError(msg)
- yield header
- def _reject_connection_header(headers: Iterable[Header], hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
- """
- Raises a ProtocolError if the Connection header is present in a header
- block.
- """
- for header in headers:
- if header[0] in CONNECTION_HEADERS:
- msg = f"Connection-specific header field present: {header[0]!r}."
- raise ProtocolError(msg)
- yield header
- def _assert_header_in_set(bytes_header: bytes,
- header_set: set[bytes | str] | set[bytes] | set[str]) -> None:
- """
- Given a set of header names, checks whether the string or byte version of
- the header name is present. Raises a Protocol error with the appropriate
- error if it's missing.
- """
- if bytes_header not in header_set:
- msg = f"Header block missing mandatory {bytes_header!r} header"
- raise ProtocolError(msg)
- def _reject_pseudo_header_fields(headers: Iterable[Header],
- hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
- """
- Raises a ProtocolError if duplicate pseudo-header fields are found in a
- header block or if a pseudo-header field appears in a block after an
- ordinary header field.
- Raises a ProtocolError if pseudo-header fields are found in trailers.
- """
- seen_pseudo_header_fields = set()
- seen_regular_header = False
- method = None
- for header in headers:
- if header[0][0] == SIGIL:
- if header[0] in seen_pseudo_header_fields:
- msg = f"Received duplicate pseudo-header field {header[0]!r}"
- raise ProtocolError(msg)
- seen_pseudo_header_fields.add(header[0])
- if seen_regular_header:
- msg = f"Received pseudo-header field out of sequence: {header[0]!r}"
- raise ProtocolError(msg)
- if header[0] not in _ALLOWED_PSEUDO_HEADER_FIELDS:
- msg = f"Received custom pseudo-header field {header[0]!r}"
- raise ProtocolError(msg)
- if header[0] in b":method":
- method = header[1]
- else:
- seen_regular_header = True
- yield header
- # Check the pseudo-headers we got to confirm they're acceptable.
- _check_pseudo_header_field_acceptability(
- seen_pseudo_header_fields, method, hdr_validation_flags,
- )
- def _check_pseudo_header_field_acceptability(pseudo_headers: set[bytes | str] | set[bytes] | set[str],
- method: bytes | None,
- hdr_validation_flags: HeaderValidationFlags) -> None:
- """
- Given the set of pseudo-headers present in a header block and the
- validation flags, confirms that RFC 7540 allows them.
- """
- # Pseudo-header fields MUST NOT appear in trailers - RFC 7540 § 8.1.2.1
- if hdr_validation_flags.is_trailer and pseudo_headers:
- msg = f"Received pseudo-header in trailer {pseudo_headers}"
- raise ProtocolError(msg)
- # If ':status' pseudo-header is not there in a response header, reject it.
- # Similarly, if ':path', ':method', or ':scheme' are not there in a request
- # header, reject it. Additionally, if a response contains any request-only
- # headers or vice-versa, reject it.
- # Relevant RFC section: RFC 7540 § 8.1.2.4
- # https://tools.ietf.org/html/rfc7540#section-8.1.2.4
- if hdr_validation_flags.is_response_header:
- _assert_header_in_set(b":status", pseudo_headers)
- invalid_response_headers = pseudo_headers & _REQUEST_ONLY_HEADERS
- if invalid_response_headers:
- msg = f"Encountered request-only headers {invalid_response_headers}"
- raise ProtocolError(msg)
- elif (not hdr_validation_flags.is_response_header and
- not hdr_validation_flags.is_trailer):
- # This is a request, so we need to have seen :path, :method, and
- # :scheme.
- _assert_header_in_set(b":path", pseudo_headers)
- _assert_header_in_set(b":method", pseudo_headers)
- _assert_header_in_set(b":scheme", pseudo_headers)
- invalid_request_headers = pseudo_headers & _RESPONSE_ONLY_HEADERS
- if invalid_request_headers:
- msg = f"Encountered response-only headers {invalid_request_headers}"
- raise ProtocolError(msg)
- if method != b"CONNECT":
- invalid_headers = pseudo_headers & _CONNECT_REQUEST_ONLY_HEADERS
- if invalid_headers:
- msg = f"Encountered connect-request-only headers {invalid_headers!r}"
- raise ProtocolError(msg)
- def _validate_host_authority_header(headers: Iterable[Header]) -> Generator[Header, None, None]:
- """
- Given the :authority and Host headers from a request block that isn't
- a trailer, check that:
- 1. At least one of these headers is set.
- 2. If both headers are set, they match.
- :param headers: The HTTP header set.
- :raises: ``ProtocolError``
- """
- # We use None as a sentinel value. Iterate over the list of headers,
- # and record the value of these headers (if present). We don't need
- # to worry about receiving duplicate :authority headers, as this is
- # enforced by the _reject_pseudo_header_fields() pipeline.
- #
- # TODO: We should also guard against receiving duplicate Host headers,
- # and against sending duplicate headers.
- authority_header_val = None
- host_header_val = None
- for header in headers:
- if header[0] == b":authority":
- authority_header_val = header[1]
- elif header[0] == b"host":
- host_header_val = header[1]
- yield header
- # If we have not-None values for these variables, then we know we saw
- # the corresponding header.
- authority_present = (authority_header_val is not None)
- host_present = (host_header_val is not None)
- # It is an error for a request header block to contain neither
- # an :authority header nor a Host header.
- if not authority_present and not host_present:
- msg = "Request header block does not have an :authority or Host header."
- raise ProtocolError(msg)
- # If we receive both headers, they should definitely match.
- if authority_present and host_present and authority_header_val != host_header_val:
- msg = (
- "Request header block has mismatched :authority and "
- f"Host headers: {authority_header_val!r} / {host_header_val!r}"
- )
- raise ProtocolError(msg)
- def _check_host_authority_header(headers: Iterable[Header],
- hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
- """
- Raises a ProtocolError if a header block arrives that does not contain an
- :authority or a Host header, or if a header block contains both fields,
- but their values do not match.
- """
- # We only expect to see :authority and Host headers on request header
- # blocks that aren't trailers, so skip this validation if this is a
- # response header or we're looking at trailer blocks.
- skip_validation = (
- hdr_validation_flags.is_response_header or
- hdr_validation_flags.is_trailer
- )
- if skip_validation:
- return (h for h in headers)
- return _validate_host_authority_header(headers)
- def _check_path_header(headers: Iterable[Header],
- hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
- """
- Raise a ProtocolError if a header block arrives or is sent that contains an
- empty :path header.
- """
- def inner() -> Generator[Header, None, None]:
- for header in headers:
- if header[0] == b":path" and not header[1]:
- msg = "An empty :path header is forbidden"
- raise ProtocolError(msg)
- yield header
- # We only expect to see :authority and Host headers on request header
- # blocks that aren't trailers, so skip this validation if this is a
- # response header or we're looking at trailer blocks.
- skip_validation = (
- hdr_validation_flags.is_response_header or
- hdr_validation_flags.is_trailer
- )
- if skip_validation:
- return (h for h in headers)
- return inner()
- def _to_bytes(v: bytes | str) -> bytes:
- """
- Given an assumed `str` (or anything that supports `.encode()`),
- encodes it using utf-8 into bytes. Returns the unmodified object
- if it is already a `bytes` object.
- """
- return v if isinstance(v, bytes) else v.encode("utf-8")
- def utf8_encode_headers(headers: Iterable[HeaderWeaklyTyped]) -> list[Header]:
- """
- Given an iterable of header two-tuples, rebuilds that as a list with the
- header names and values encoded as utf-8 bytes. This function produces
- tuples that preserve the original type of the header tuple for tuple and
- any ``HeaderTuple``.
- """
- encoded_headers: list[Header] = []
- for header in headers:
- h = (_to_bytes(header[0]), _to_bytes(header[1]))
- if isinstance(header, HeaderTuple):
- encoded_headers.append(header.__class__(h[0], h[1]))
- else:
- encoded_headers.append(h)
- return encoded_headers
- def _lowercase_header_names(headers: Iterable[Header],
- hdr_validation_flags: HeaderValidationFlags | None) -> Generator[Header, None, None]:
- """
- Given an iterable of header two-tuples, rebuilds that iterable with the
- header names lowercased. This generator produces tuples that preserve the
- original type of the header tuple for tuple and any ``HeaderTuple``.
- """
- for header in headers:
- if isinstance(header, HeaderTuple):
- yield header.__class__(header[0].lower(), header[1])
- else:
- yield (header[0].lower(), header[1])
- def _strip_surrounding_whitespace(headers: Iterable[Header],
- hdr_validation_flags: HeaderValidationFlags | None) -> Generator[Header, None, None]:
- """
- Given an iterable of header two-tuples, strip both leading and trailing
- whitespace from both header names and header values. This generator
- produces tuples that preserve the original type of the header tuple for
- tuple and any ``HeaderTuple``.
- """
- for header in headers:
- if isinstance(header, HeaderTuple):
- yield header.__class__(header[0].strip(), header[1].strip())
- else:
- yield (header[0].strip(), header[1].strip())
- def _strip_connection_headers(headers: Iterable[Header],
- hdr_validation_flags: HeaderValidationFlags | None) -> Generator[Header, None, None]:
- """
- Strip any connection headers as per RFC7540 § 8.1.2.2.
- """
- for header in headers:
- if header[0] not in CONNECTION_HEADERS:
- yield header
- def _check_sent_host_authority_header(headers: Iterable[Header],
- hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
- """
- Raises an InvalidHeaderBlockError if we try to send a header block
- that does not contain an :authority or a Host header, or if
- the header block contains both fields, but their values do not match.
- """
- # We only expect to see :authority and Host headers on request header
- # blocks that aren't trailers, so skip this validation if this is a
- # response header or we're looking at trailer blocks.
- skip_validation = (
- hdr_validation_flags.is_response_header or
- hdr_validation_flags.is_trailer
- )
- if skip_validation:
- return (h for h in headers)
- return _validate_host_authority_header(headers)
- def _combine_cookie_fields(headers: Iterable[Header], hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
- """
- RFC 7540 § 8.1.2.5 allows HTTP/2 clients to split the Cookie header field,
- which must normally appear only once, into multiple fields for better
- compression. However, they MUST be joined back up again when received.
- This normalization step applies that transform. The side-effect is that
- all cookie fields now appear *last* in the header block.
- """
- # There is a problem here about header indexing. Specifically, it's
- # possible that all these cookies are sent with different header indexing
- # values. At this point it shouldn't matter too much, so we apply our own
- # logic and make them never-indexed.
- cookies: list[bytes] = []
- for header in headers:
- if header[0] == b"cookie":
- cookies.append(header[1])
- else:
- yield header
- if cookies:
- cookie_val = b"; ".join(cookies)
- yield NeverIndexedHeaderTuple(b"cookie", cookie_val)
- def _split_outbound_cookie_fields(headers: Iterable[Header],
- hdr_validation_flags: HeaderValidationFlags | None) -> Generator[Header, None, None]:
- """
- RFC 7540 § 8.1.2.5 allows for better compression efficiency,
- to split the Cookie header field into separate header fields
- We want to do it for outbound requests, as we are doing for
- inbound.
- """
- for header in headers:
- assert isinstance(header[0], bytes)
- assert isinstance(header[1], bytes)
- if header[0] == b"cookie":
- for cookie_val in header[1].split(b"; "):
- if isinstance(header, HeaderTuple):
- yield header.__class__(header[0], cookie_val)
- else:
- yield header[0], cookie_val
- else:
- yield header
- def normalize_outbound_headers(headers: Iterable[Header],
- hdr_validation_flags: HeaderValidationFlags | None,
- should_split_outbound_cookies: bool=False) -> Generator[Header, None, None]:
- """
- Normalizes a header sequence that we are about to send.
- :param headers: The HTTP header set.
- :param hdr_validation_flags: An instance of HeaderValidationFlags.
- :param should_split_outbound_cookies: boolean flag
- """
- headers = _lowercase_header_names(headers, hdr_validation_flags)
- if should_split_outbound_cookies:
- headers = _split_outbound_cookie_fields(headers, hdr_validation_flags)
- headers = _strip_surrounding_whitespace(headers, hdr_validation_flags)
- headers = _strip_connection_headers(headers, hdr_validation_flags)
- return _secure_headers(headers, hdr_validation_flags)
- def normalize_inbound_headers(headers: Iterable[Header],
- hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
- """
- Normalizes a header sequence that we have received.
- :param headers: The HTTP header set.
- :param hdr_validation_flags: An instance of HeaderValidationFlags
- """
- return _combine_cookie_fields(headers, hdr_validation_flags)
- def validate_outbound_headers(headers: Iterable[Header],
- hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
- """
- Validates and normalizes a header sequence that we are about to send.
- :param headers: The HTTP header set.
- :param hdr_validation_flags: An instance of HeaderValidationFlags.
- """
- headers = _reject_te(
- headers, hdr_validation_flags,
- )
- headers = _reject_connection_header(
- headers, hdr_validation_flags,
- )
- headers = _reject_pseudo_header_fields(
- headers, hdr_validation_flags,
- )
- headers = _check_sent_host_authority_header(
- headers, hdr_validation_flags,
- )
- return _check_path_header(headers, hdr_validation_flags)
- class SizeLimitDict(collections.OrderedDict[int, Any]):
- def __init__(self, *args: dict[int, int], **kwargs: Any) -> None:
- self._size_limit = kwargs.pop("size_limit", None)
- super().__init__(*args, **kwargs)
- self._check_size_limit()
- def __setitem__(self, key: int, value: Any | int) -> None:
- super().__setitem__(key, value)
- self._check_size_limit()
- def _check_size_limit(self) -> None:
- if self._size_limit is not None:
- while len(self) > self._size_limit:
- self.popitem(last=False)
|