Merge pull request #2723 from bluetech/perf1
httputil: A couple of small performance improvements
This commit is contained in:
commit
ff985fe509
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue