Merge pull request #2723 from bluetech/perf1

httputil: A couple of small performance improvements
This commit is contained in:
Ben Darnell 2019-09-02 13:20:28 -04:00 committed by GitHub
commit ff985fe509
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 24 additions and 37 deletions

View File

@ -24,6 +24,7 @@ import collections
import copy
import datetime
import email.utils
from functools import lru_cache
from http.client import responses
import http.cookies
import re
@ -62,37 +63,14 @@ if typing.TYPE_CHECKING:
import unittest # noqa: F401
class _NormalizedHeaderCache(dict):
"""Dynamic cached mapping of header names to Http-Header-Case.
@lru_cache(1000)
def _normalize_header(name: str) -> str:
"""Map a header name to Http-Header-Case.
Implemented as a dict subclass so that cache hits are as fast as a
normal dict lookup, without the overhead of a python function
call.
>>> normalized_headers = _NormalizedHeaderCache(10)
>>> normalized_headers["coNtent-TYPE"]
>>> _normalize_header("coNtent-TYPE")
'Content-Type'
"""
def __init__(self, size: int) -> None:
super(_NormalizedHeaderCache, self).__init__()
self.size = size
self.queue = collections.deque() # type: Deque[str]
def __missing__(self, key: str) -> str:
normalized = "-".join([w.capitalize() for w in key.split("-")])
self[key] = normalized
self.queue.append(key)
if len(self.queue) > self.size:
# Limit the size of the cache. LRU would be better, but this
# simpler approach should be fine. In Python 2.7+ we could
# use OrderedDict (or in 3.2+, @functools.lru_cache).
old_key = self.queue.popleft()
del self[old_key]
return normalized
_normalized_headers = _NormalizedHeaderCache(1000)
return "-".join([w.capitalize() for w in name.split("-")])
class HTTPHeaders(collections.abc.MutableMapping):
@ -143,7 +121,7 @@ class HTTPHeaders(collections.abc.MutableMapping):
def __init__(self, *args: typing.Any, **kwargs: str) -> None: # noqa: F811
self._dict = {} # type: typing.Dict[str, str]
self._as_list = {} # type: typing.Dict[str, typing.List[str]]
self._last_key = None
self._last_key = None # type: Optional[str]
if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], HTTPHeaders):
# Copy constructor
for k, v in args[0].get_all():
@ -156,7 +134,7 @@ class HTTPHeaders(collections.abc.MutableMapping):
def add(self, name: str, value: str) -> None:
"""Adds a new value for the given key."""
norm_name = _normalized_headers[name]
norm_name = _normalize_header(name)
self._last_key = norm_name
if norm_name in self:
self._dict[norm_name] = (
@ -168,7 +146,7 @@ class HTTPHeaders(collections.abc.MutableMapping):
def get_list(self, name: str) -> List[str]:
"""Returns all values for the given header as a list."""
norm_name = _normalized_headers[name]
norm_name = _normalize_header(name)
return self._as_list.get(norm_name, [])
def get_all(self) -> Iterable[Tuple[str, str]]:
@ -230,15 +208,15 @@ class HTTPHeaders(collections.abc.MutableMapping):
# MutableMapping abstract method implementations.
def __setitem__(self, name: str, value: str) -> None:
norm_name = _normalized_headers[name]
norm_name = _normalize_header(name)
self._dict[norm_name] = value
self._as_list[norm_name] = [value]
def __getitem__(self, name: str) -> str:
return self._dict[_normalized_headers[name]]
return self._dict[_normalize_header(name)]
def __delitem__(self, name: str) -> None:
norm_name = _normalized_headers[name]
norm_name = _normalize_header(name)
del self._dict[norm_name]
del self._as_list[norm_name]
@ -896,6 +874,9 @@ RequestStartLine = collections.namedtuple(
)
_http_version_re = re.compile(r"^HTTP/1\.[0-9]$")
def parse_request_start_line(line: str) -> RequestStartLine:
"""Returns a (method, path, version) tuple for an HTTP 1.x request line.
@ -910,7 +891,7 @@ def parse_request_start_line(line: str) -> RequestStartLine:
# https://tools.ietf.org/html/rfc7230#section-3.1.1
# invalid request-line SHOULD respond with a 400 (Bad Request)
raise HTTPInputError("Malformed HTTP request line")
if not re.match(r"^HTTP/1\.[0-9]$", version):
if not _http_version_re.match(version):
raise HTTPInputError(
"Malformed HTTP version in HTTP Request-Line: %r" % version
)
@ -922,6 +903,9 @@ ResponseStartLine = collections.namedtuple(
)
_http_response_line_re = re.compile(r"(HTTP/1.[0-9]) ([0-9]+) ([^\r]*)")
def parse_response_start_line(line: str) -> ResponseStartLine:
"""Returns a (version, code, reason) tuple for an HTTP 1.x response line.
@ -931,7 +915,7 @@ def parse_response_start_line(line: str) -> ResponseStartLine:
ResponseStartLine(version='HTTP/1.1', code=200, reason='OK')
"""
line = native_str(line)
match = re.match("(HTTP/1.[0-9]) ([0-9]+) ([^\r]*)", line)
match = _http_response_line_re.match(line)
if not match:
raise HTTPInputError("Error parsing response start line")
return ResponseStartLine(match.group(1), int(match.group(2)), match.group(3))
@ -1036,6 +1020,9 @@ def doctests():
return doctest.DocTestSuite()
_netloc_re = re.compile(r"^(.+):(\d+)$")
def split_host_and_port(netloc: str) -> Tuple[str, Optional[int]]:
"""Returns ``(host, port)`` tuple from ``netloc``.
@ -1043,7 +1030,7 @@ def split_host_and_port(netloc: str) -> Tuple[str, Optional[int]]:
.. versionadded:: 4.1
"""
match = re.match(r"^(.+):(\d+)$", netloc)
match = _netloc_re.match(netloc)
if match:
host = match.group(1)
port = int(match.group(2)) # type: Optional[int]