mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-27 02:24:18 +00:00
A clearer implementation of MultiDictView
This makes MultiDictView work with a simple getter/setter pair, rather than using attributes with implicit leading underscores. Also move MultiDictView into multidict.py and adds some simple unit tests.
This commit is contained in:
parent
96d8ec1ee3
commit
a5c4cd0340
@ -2,13 +2,13 @@ from __future__ import absolute_import, print_function, division
|
|||||||
from .request import Request
|
from .request import Request
|
||||||
from .response import Response
|
from .response import Response
|
||||||
from .headers import Headers
|
from .headers import Headers
|
||||||
from .message import MultiDictView, decoded
|
from .message import decoded
|
||||||
from . import http1, http2, status_codes
|
from . import http1, http2, status_codes
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Request",
|
"Request",
|
||||||
"Response",
|
"Response",
|
||||||
"Headers",
|
"Headers",
|
||||||
"MultiDictView", "decoded",
|
"decoded",
|
||||||
"http1", "http2", "status_codes",
|
"http1", "http2", "status_codes",
|
||||||
]
|
]
|
||||||
|
@ -236,72 +236,3 @@ class decoded(object):
|
|||||||
def __exit__(self, type, value, tb):
|
def __exit__(self, type, value, tb):
|
||||||
if self.ce:
|
if self.ce:
|
||||||
self.message.encode(self.ce)
|
self.message.encode(self.ce)
|
||||||
|
|
||||||
|
|
||||||
class MultiDictView(MultiDict):
|
|
||||||
"""
|
|
||||||
Some parts in HTTP (Cookies, URL query strings, ...) require a specific data structure: A MultiDict.
|
|
||||||
It behaves mostly like an ordered dict but it can have several values for the same key.
|
|
||||||
|
|
||||||
The MultiDictView provides a MultiDict *view* on an :py:class:`Request` or :py:class:`Response`.
|
|
||||||
That is, it represents a part of the request as a MultiDict, but doesn't contain state/data themselves.
|
|
||||||
|
|
||||||
For example, ``request.cookies`` provides a view on the ``Cookie: ...`` header.
|
|
||||||
Any change to ``request.cookies`` will also modify the ``Cookie`` header.
|
|
||||||
Any change to the ``Cookie`` header will also modify ``request.cookies``.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# Cookies are represented as a MultiDict.
|
|
||||||
>>> request.cookies
|
|
||||||
MultiDictView[("name", "value"), ("a", "false"), ("a", "42")]
|
|
||||||
|
|
||||||
# MultiDicts mostly behave like a normal dict.
|
|
||||||
>>> request.cookies["name"]
|
|
||||||
"value"
|
|
||||||
|
|
||||||
# If there is more than one value, only the first value is returned.
|
|
||||||
>>> request.cookies["a"]
|
|
||||||
"false"
|
|
||||||
|
|
||||||
# `.get_all(key)` returns a list of all values.
|
|
||||||
>>> request.cookies.get_all("a")
|
|
||||||
["false", "42"]
|
|
||||||
|
|
||||||
# Changes to the headers are immediately reflected in the cookies.
|
|
||||||
>>> request.cookies
|
|
||||||
MultiDictView[("name", "value"), ...]
|
|
||||||
>>> del request.headers["Cookie"]
|
|
||||||
>>> request.cookies
|
|
||||||
MultiDictView[] # empty now
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, attr, message):
|
|
||||||
if False: # pragma: no cover
|
|
||||||
# We do not want to call the parent constructor here as that
|
|
||||||
# would cause an unnecessary parse/unparse pass.
|
|
||||||
# This is here to silence linters. Message
|
|
||||||
super(MultiDictView, self).__init__(None)
|
|
||||||
self._attr = attr
|
|
||||||
self._message = message # type: Message
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _kconv(key):
|
|
||||||
# All request-attributes are case-sensitive.
|
|
||||||
return key
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _reduce_values(values):
|
|
||||||
# We just return the first element if
|
|
||||||
# multiple elements exist with the same key.
|
|
||||||
return values[0]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fields(self):
|
|
||||||
return getattr(self._message, "_" + self._attr)
|
|
||||||
|
|
||||||
@fields.setter
|
|
||||||
def fields(self, value):
|
|
||||||
setattr(self._message, self._attr, value)
|
|
||||||
|
@ -10,8 +10,9 @@ from netlib import utils
|
|||||||
from netlib.http import cookies
|
from netlib.http import cookies
|
||||||
from netlib.odict import ODict
|
from netlib.odict import ODict
|
||||||
from .. import encoding
|
from .. import encoding
|
||||||
|
from ..multidict import MultiDictView
|
||||||
from .headers import Headers
|
from .headers import Headers
|
||||||
from .message import Message, _native, _always_bytes, MessageData, MultiDictView
|
from .message import Message, _native, _always_bytes, MessageData
|
||||||
|
|
||||||
# This regex extracts & splits the host header into host and port.
|
# This regex extracts & splits the host header into host and port.
|
||||||
# Handles the edge case of IPv6 addresses containing colons.
|
# Handles the edge case of IPv6 addresses containing colons.
|
||||||
@ -228,20 +229,25 @@ class Request(Message):
|
|||||||
"""
|
"""
|
||||||
The request query string as an :py:class:`MultiDictView` object.
|
The request query string as an :py:class:`MultiDictView` object.
|
||||||
"""
|
"""
|
||||||
return MultiDictView("query", self)
|
return MultiDictView(
|
||||||
|
self._get_query,
|
||||||
|
self._set_query
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
def _get_query(self):
|
||||||
def _query(self):
|
|
||||||
_, _, _, _, query, _ = urllib.parse.urlparse(self.url)
|
_, _, _, _, query, _ = urllib.parse.urlparse(self.url)
|
||||||
return tuple(utils.urldecode(query))
|
return tuple(utils.urldecode(query))
|
||||||
|
|
||||||
@query.setter
|
def _set_query(self, value):
|
||||||
def query(self, value):
|
|
||||||
query = utils.urlencode(value)
|
query = utils.urlencode(value)
|
||||||
scheme, netloc, path, params, _, fragment = urllib.parse.urlparse(self.url)
|
scheme, netloc, path, params, _, fragment = urllib.parse.urlparse(self.url)
|
||||||
_, _, _, self.path = utils.parse_url(
|
_, _, _, self.path = utils.parse_url(
|
||||||
urllib.parse.urlunparse([scheme, netloc, path, params, query, fragment]))
|
urllib.parse.urlunparse([scheme, netloc, path, params, query, fragment]))
|
||||||
|
|
||||||
|
@query.setter
|
||||||
|
def query(self, value):
|
||||||
|
self._set_query(value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cookies(self):
|
def cookies(self):
|
||||||
# type: () -> MultiDictView
|
# type: () -> MultiDictView
|
||||||
@ -250,16 +256,21 @@ class Request(Message):
|
|||||||
|
|
||||||
An empty :py:class:`MultiDictView` object if the cookie monster ate them all.
|
An empty :py:class:`MultiDictView` object if the cookie monster ate them all.
|
||||||
"""
|
"""
|
||||||
return MultiDictView("cookies", self)
|
return MultiDictView(
|
||||||
|
self._get_cookies,
|
||||||
|
self._set_cookies
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
def _get_cookies(self):
|
||||||
def _cookies(self):
|
|
||||||
h = self.headers.get_all("Cookie")
|
h = self.headers.get_all("Cookie")
|
||||||
return tuple(cookies.parse_cookie_headers(h))
|
return tuple(cookies.parse_cookie_headers(h))
|
||||||
|
|
||||||
|
def _set_cookies(self, value):
|
||||||
|
self.headers["cookie"] = cookies.format_cookie_header(value)
|
||||||
|
|
||||||
@cookies.setter
|
@cookies.setter
|
||||||
def cookies(self, value):
|
def cookies(self, value):
|
||||||
self.headers["cookie"] = cookies.format_cookie_header(value)
|
self._set_cookies(value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_components(self):
|
def path_components(self):
|
||||||
@ -322,17 +333,18 @@ class Request(Message):
|
|||||||
An empty MultiDictView if the content-type indicates non-form data
|
An empty MultiDictView if the content-type indicates non-form data
|
||||||
or the content could not be parsed.
|
or the content could not be parsed.
|
||||||
"""
|
"""
|
||||||
return MultiDictView("urlencoded_form", self)
|
return MultiDictView(
|
||||||
|
self._get_urlencoded_form,
|
||||||
|
self._set_urlencoded_form
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
def _get_urlencoded_form(self):
|
||||||
def _urlencoded_form(self):
|
|
||||||
is_valid_content_type = "application/x-www-form-urlencoded" in self.headers.get("content-type", "").lower()
|
is_valid_content_type = "application/x-www-form-urlencoded" in self.headers.get("content-type", "").lower()
|
||||||
if is_valid_content_type:
|
if is_valid_content_type:
|
||||||
return tuple(utils.urldecode(self.content))
|
return tuple(utils.urldecode(self.content))
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
@urlencoded_form.setter
|
def _set_urlencoded_form(self, value):
|
||||||
def urlencoded_form(self, value):
|
|
||||||
"""
|
"""
|
||||||
Sets the body to the URL-encoded form data, and adds the appropriate content-type header.
|
Sets the body to the URL-encoded form data, and adds the appropriate content-type header.
|
||||||
This will overwrite the existing content if there is one.
|
This will overwrite the existing content if there is one.
|
||||||
@ -340,21 +352,30 @@ class Request(Message):
|
|||||||
self.headers["content-type"] = "application/x-www-form-urlencoded"
|
self.headers["content-type"] = "application/x-www-form-urlencoded"
|
||||||
self.content = utils.urlencode(value)
|
self.content = utils.urlencode(value)
|
||||||
|
|
||||||
|
@urlencoded_form.setter
|
||||||
|
def urlencoded_form(self, value):
|
||||||
|
self._set_urlencoded_form(value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def multipart_form(self):
|
def multipart_form(self):
|
||||||
"""
|
"""
|
||||||
The multipart form data as an :py:class:`MultipartFormDict` object.
|
The multipart form data as an :py:class:`MultipartFormDict` object.
|
||||||
None if the content-type indicates non-form data.
|
None if the content-type indicates non-form data.
|
||||||
"""
|
"""
|
||||||
return MultiDictView("multipart_form", self)
|
return MultiDictView(
|
||||||
|
self._get_multipart_form,
|
||||||
|
self._set_multipart_form
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
def _get_multipart_form(self):
|
||||||
def _multipart_form(self):
|
|
||||||
is_valid_content_type = "multipart/form-data" in self.headers.get("content-type", "").lower()
|
is_valid_content_type = "multipart/form-data" in self.headers.get("content-type", "").lower()
|
||||||
if is_valid_content_type:
|
if is_valid_content_type:
|
||||||
return utils.multipartdecode(self.headers, self.content)
|
return utils.multipartdecode(self.headers, self.content)
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
|
def _set_multipart_form(self, value):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
@multipart_form.setter
|
@multipart_form.setter
|
||||||
def multipart_form(self, value):
|
def multipart_form(self, value):
|
||||||
raise NotImplementedError()
|
self._set_multipart_form(value)
|
||||||
|
@ -5,7 +5,8 @@ import time
|
|||||||
|
|
||||||
from . import cookies
|
from . import cookies
|
||||||
from .headers import Headers
|
from .headers import Headers
|
||||||
from .message import Message, _native, _always_bytes, MessageData, MultiDictView
|
from .message import Message, _native, _always_bytes, MessageData
|
||||||
|
from ..multidict import MultiDictView
|
||||||
from .. import utils
|
from .. import utils
|
||||||
|
|
||||||
|
|
||||||
@ -80,21 +81,26 @@ class Response(Message):
|
|||||||
Caveats:
|
Caveats:
|
||||||
Updating the attr
|
Updating the attr
|
||||||
"""
|
"""
|
||||||
return MultiDictView("cookies", self)
|
return MultiDictView(
|
||||||
|
self._get_cookies,
|
||||||
|
self._set_cookies
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
def _get_cookies(self):
|
||||||
def _cookies(self):
|
|
||||||
h = self.headers.get_all("set-cookie")
|
h = self.headers.get_all("set-cookie")
|
||||||
return tuple(cookies.parse_set_cookie_headers(h))
|
return tuple(cookies.parse_set_cookie_headers(h))
|
||||||
|
|
||||||
@cookies.setter
|
def _set_cookies(self, value):
|
||||||
def cookies(self, all_cookies):
|
|
||||||
cookie_headers = []
|
cookie_headers = []
|
||||||
for k, v in all_cookies:
|
for k, v in value:
|
||||||
header = cookies.format_set_cookie_header(k, v[0], v[1])
|
header = cookies.format_set_cookie_header(k, v[0], v[1])
|
||||||
cookie_headers.append(header)
|
cookie_headers.append(header)
|
||||||
self.headers.set_all("set-cookie", cookie_headers)
|
self.headers.set_all("set-cookie", cookie_headers)
|
||||||
|
|
||||||
|
@cookies.setter
|
||||||
|
def cookies(self, value):
|
||||||
|
self._set_cookies(value)
|
||||||
|
|
||||||
def refresh(self, now=None):
|
def refresh(self, now=None):
|
||||||
"""
|
"""
|
||||||
This fairly complex and heuristic function refreshes a server
|
This fairly complex and heuristic function refreshes a server
|
||||||
|
@ -15,13 +15,7 @@ from .utils import Serializable
|
|||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(ABCMeta)
|
@six.add_metaclass(ABCMeta)
|
||||||
class MultiDict(MutableMapping, Serializable):
|
class _MultiDict(MutableMapping, Serializable):
|
||||||
def __init__(self, fields=None):
|
|
||||||
|
|
||||||
# it is important for us that .fields is immutable, so that we can easily
|
|
||||||
# detect changes to it.
|
|
||||||
self.fields = tuple(fields) if fields else tuple() # type: Tuple[Tuple[bytes, bytes], ...]
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
fields = tuple(
|
fields = tuple(
|
||||||
repr(field)
|
repr(field)
|
||||||
@ -173,7 +167,7 @@ class MultiDict(MutableMapping, Serializable):
|
|||||||
if multi:
|
if multi:
|
||||||
return self.fields
|
return self.fields
|
||||||
else:
|
else:
|
||||||
return super(MultiDict, self).items()
|
return super(_MultiDict, self).items()
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""
|
"""
|
||||||
@ -213,6 +207,12 @@ class MultiDict(MutableMapping, Serializable):
|
|||||||
return cls(tuple(x) for x in state)
|
return cls(tuple(x) for x in state)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiDict(_MultiDict):
|
||||||
|
def __init__(self, fields=None):
|
||||||
|
super(MultiDict, self).__init__()
|
||||||
|
self.fields = tuple(fields) if fields else tuple() # type: Tuple[Tuple[bytes, bytes], ...]
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(ABCMeta)
|
@six.add_metaclass(ABCMeta)
|
||||||
class ImmutableMultiDict(MultiDict):
|
class ImmutableMultiDict(MultiDict):
|
||||||
def _immutable(self, *_):
|
def _immutable(self, *_):
|
||||||
@ -246,3 +246,34 @@ class ImmutableMultiDict(MultiDict):
|
|||||||
ret = self.copy()
|
ret = self.copy()
|
||||||
super(ImmutableMultiDict, ret).insert(index, key, value)
|
super(ImmutableMultiDict, ret).insert(index, key, value)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class MultiDictView(_MultiDict):
|
||||||
|
"""
|
||||||
|
The MultiDictView provides the MultiDict interface over calculated data.
|
||||||
|
The view itself contains no state - data is retrieved from the parent on
|
||||||
|
request, and stored back to the parent on change.
|
||||||
|
"""
|
||||||
|
def __init__(self, getter, setter):
|
||||||
|
self._getter = getter
|
||||||
|
self._setter = setter
|
||||||
|
super(MultiDictView, self).__init__()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _kconv(key):
|
||||||
|
# All request-attributes are case-sensitive.
|
||||||
|
return key
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _reduce_values(values):
|
||||||
|
# We just return the first element if
|
||||||
|
# multiple elements exist with the same key.
|
||||||
|
return values[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fields(self):
|
||||||
|
return self._getter()
|
||||||
|
|
||||||
|
@fields.setter
|
||||||
|
def fields(self, value):
|
||||||
|
return self._setter(value)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from netlib import tutils
|
from netlib import tutils
|
||||||
from netlib.multidict import MultiDict, ImmutableMultiDict
|
from netlib.multidict import MultiDict, ImmutableMultiDict, MultiDictView
|
||||||
|
|
||||||
|
|
||||||
class _TMulti(object):
|
class _TMulti(object):
|
||||||
@ -214,4 +214,26 @@ class TestImmutableMultiDict(object):
|
|||||||
def test_with_insert(self):
|
def test_with_insert(self):
|
||||||
md = TImmutableMultiDict()
|
md = TImmutableMultiDict()
|
||||||
assert md.with_insert(0, "foo", "bar").fields == (("foo", "bar"),)
|
assert md.with_insert(0, "foo", "bar").fields == (("foo", "bar"),)
|
||||||
assert md.fields == ()
|
|
||||||
|
|
||||||
|
class TParent(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.vals = tuple()
|
||||||
|
|
||||||
|
def setter(self, vals):
|
||||||
|
self.vals = vals
|
||||||
|
|
||||||
|
def getter(self):
|
||||||
|
return self.vals
|
||||||
|
|
||||||
|
|
||||||
|
class TestMultiDictView(object):
|
||||||
|
def test_modify(self):
|
||||||
|
p = TParent()
|
||||||
|
tv = MultiDictView(p.getter, p.setter)
|
||||||
|
assert len(tv) == 0
|
||||||
|
tv["a"] = "b"
|
||||||
|
assert p.vals == (("a", "b"),)
|
||||||
|
tv["c"] = "b"
|
||||||
|
assert p.vals == (("a", "b"), ("c", "b"))
|
||||||
|
assert tv["a"] == "b"
|
||||||
|
Loading…
Reference in New Issue
Block a user