diff options
| author | Aldo Cortesi <aldo@nullcube.com> | 2016-05-21 11:37:36 +1200 | 
|---|---|---|
| committer | Aldo Cortesi <aldo@nullcube.com> | 2016-05-21 11:37:36 +1200 | 
| commit | a5c4cd034081d7dcdbd4b46bd69718edb45d4719 (patch) | |
| tree | 52accc866d25c735e6c36f05255bb3f5349f8ac7 | |
| parent | 96d8ec1ee33b076a472afc3053fdd8256559fcc3 (diff) | |
| download | mitmproxy-a5c4cd034081d7dcdbd4b46bd69718edb45d4719.tar.gz mitmproxy-a5c4cd034081d7dcdbd4b46bd69718edb45d4719.tar.bz2 mitmproxy-a5c4cd034081d7dcdbd4b46bd69718edb45d4719.zip | |
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.
| -rw-r--r-- | netlib/http/__init__.py | 4 | ||||
| -rw-r--r-- | netlib/http/message.py | 69 | ||||
| -rw-r--r-- | netlib/http/request.py | 59 | ||||
| -rw-r--r-- | netlib/http/response.py | 20 | ||||
| -rw-r--r-- | netlib/multidict.py | 49 | ||||
| -rw-r--r-- | test/netlib/test_multidict.py | 26 | 
6 files changed, 119 insertions, 108 deletions
| diff --git a/netlib/http/__init__.py b/netlib/http/__init__.py index 9fafa28f..c4eb1d58 100644 --- a/netlib/http/__init__.py +++ b/netlib/http/__init__.py @@ -2,13 +2,13 @@ from __future__ import absolute_import, print_function, division  from .request import Request  from .response import Response  from .headers import Headers -from .message import MultiDictView, decoded +from .message import decoded  from . import http1, http2, status_codes  __all__ = [      "Request",      "Response",      "Headers", -    "MultiDictView", "decoded", +    "decoded",      "http1", "http2", "status_codes",  ] diff --git a/netlib/http/message.py b/netlib/http/message.py index db4054b1..9b0180cf 100644 --- a/netlib/http/message.py +++ b/netlib/http/message.py @@ -236,72 +236,3 @@ class decoded(object):      def __exit__(self, type, value, tb):          if 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) diff --git a/netlib/http/request.py b/netlib/http/request.py index ae28084b..056a2d93 100644 --- a/netlib/http/request.py +++ b/netlib/http/request.py @@ -10,8 +10,9 @@ from netlib import utils  from netlib.http import cookies  from netlib.odict import ODict  from .. import encoding +from ..multidict import MultiDictView  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.  # 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.          """ -        return MultiDictView("query", self) +        return MultiDictView( +            self._get_query, +            self._set_query +        ) -    @property -    def _query(self): +    def _get_query(self):          _, _, _, _, query, _ = urllib.parse.urlparse(self.url)          return tuple(utils.urldecode(query)) -    @query.setter -    def query(self, value): +    def _set_query(self, value):          query = utils.urlencode(value)          scheme, netloc, path, params, _, fragment = urllib.parse.urlparse(self.url)          _, _, _, self.path = utils.parse_url(                  urllib.parse.urlunparse([scheme, netloc, path, params, query, fragment])) +    @query.setter +    def query(self, value): +        self._set_query(value) +      @property      def cookies(self):          # type: () -> MultiDictView @@ -250,16 +256,21 @@ class Request(Message):          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 _cookies(self): +    def _get_cookies(self):          h = self.headers.get_all("Cookie")          return tuple(cookies.parse_cookie_headers(h)) +    def _set_cookies(self, value): +        self.headers["cookie"] = cookies.format_cookie_header(value) +      @cookies.setter      def cookies(self, value): -        self.headers["cookie"] = cookies.format_cookie_header(value) +        self._set_cookies(value)      @property      def path_components(self): @@ -322,17 +333,18 @@ class Request(Message):          An empty MultiDictView if the content-type indicates non-form data          or the content could not be parsed.          """ -        return MultiDictView("urlencoded_form", self) +        return MultiDictView( +            self._get_urlencoded_form, +            self._set_urlencoded_form +        ) -    @property -    def _urlencoded_form(self): +    def _get_urlencoded_form(self):          is_valid_content_type = "application/x-www-form-urlencoded" in self.headers.get("content-type", "").lower()          if is_valid_content_type:              return tuple(utils.urldecode(self.content))          return () -    @urlencoded_form.setter -    def urlencoded_form(self, value): +    def _set_urlencoded_form(self, value):          """          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. @@ -340,21 +352,30 @@ class Request(Message):          self.headers["content-type"] = "application/x-www-form-urlencoded"          self.content = utils.urlencode(value) +    @urlencoded_form.setter +    def urlencoded_form(self, value): +        self._set_urlencoded_form(value) +      @property      def multipart_form(self):          """          The multipart form data as an :py:class:`MultipartFormDict` object.          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 _multipart_form(self): +    def _get_multipart_form(self):          is_valid_content_type = "multipart/form-data" in self.headers.get("content-type", "").lower()          if is_valid_content_type:              return utils.multipartdecode(self.headers, self.content)          return () +    def _set_multipart_form(self, value): +        raise NotImplementedError() +      @multipart_form.setter      def multipart_form(self, value): -        raise NotImplementedError() +        self._set_multipart_form(value) diff --git a/netlib/http/response.py b/netlib/http/response.py index 6d56fc1f..7d272e10 100644 --- a/netlib/http/response.py +++ b/netlib/http/response.py @@ -5,7 +5,8 @@ import time  from . import cookies  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 @@ -80,21 +81,26 @@ class Response(Message):          Caveats:              Updating the attr          """ -        return MultiDictView("cookies", self) +        return MultiDictView( +            self._get_cookies, +            self._set_cookies +        ) -    @property -    def _cookies(self): +    def _get_cookies(self):          h = self.headers.get_all("set-cookie")          return tuple(cookies.parse_set_cookie_headers(h)) -    @cookies.setter -    def cookies(self, all_cookies): +    def _set_cookies(self, value):          cookie_headers = [] -        for k, v in all_cookies: +        for k, v in value:              header = cookies.format_set_cookie_header(k, v[0], v[1])              cookie_headers.append(header)          self.headers.set_all("set-cookie", cookie_headers) +    @cookies.setter +    def cookies(self, value): +        self._set_cookies(value) +      def refresh(self, now=None):          """          This fairly complex and heuristic function refreshes a server diff --git a/netlib/multidict.py b/netlib/multidict.py index a359d46b..3af7979b 100644 --- a/netlib/multidict.py +++ b/netlib/multidict.py @@ -15,13 +15,7 @@ from .utils import Serializable  @six.add_metaclass(ABCMeta) -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], ...] - +class _MultiDict(MutableMapping, Serializable):      def __repr__(self):          fields = tuple(              repr(field) @@ -97,7 +91,7 @@ class MultiDict(MutableMapping, Serializable):              value              for k, value in self.fields              if self._kconv(k) == key -            ] +        ]      def set_all(self, key, values):          """ @@ -173,7 +167,7 @@ class MultiDict(MutableMapping, Serializable):          if multi:              return self.fields          else: -            return super(MultiDict, self).items() +            return super(_MultiDict, self).items()      def to_dict(self):          """ @@ -213,6 +207,12 @@ class MultiDict(MutableMapping, Serializable):          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)  class ImmutableMultiDict(MultiDict):      def _immutable(self, *_): @@ -246,3 +246,34 @@ class ImmutableMultiDict(MultiDict):          ret = self.copy()          super(ImmutableMultiDict, ret).insert(index, key, value)          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) diff --git a/test/netlib/test_multidict.py b/test/netlib/test_multidict.py index ceea3806..5bb65e3f 100644 --- a/test/netlib/test_multidict.py +++ b/test/netlib/test_multidict.py @@ -1,5 +1,5 @@  from netlib import tutils -from netlib.multidict import MultiDict, ImmutableMultiDict +from netlib.multidict import MultiDict, ImmutableMultiDict, MultiDictView  class _TMulti(object): @@ -214,4 +214,26 @@ class TestImmutableMultiDict(object):      def test_with_insert(self):          md = TImmutableMultiDict()          assert md.with_insert(0, "foo", "bar").fields == (("foo", "bar"),) -        assert md.fields == ()
\ No newline at end of file + + +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" | 
