From 9b6a5289cfb7141214d22a23df15bfb0565e65f9 Mon Sep 17 00:00:00 2001 From: Sergey Skripnick Date: Tue, 31 Oct 2017 19:02:08 +0200 Subject: [PATCH] Content-Disposition fast access in ClientResponse Add ContentDisposition class and content_disposition property Partially implements #1670 --- CHANGES/2455.feature | 1 + aiohttp/client_reqrep.py | 20 +++++++++++++++++-- docs/client_reference.rst | 24 ++++++++++++++++++++++- tests/test_client_response.py | 36 +++++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 CHANGES/2455.feature diff --git a/CHANGES/2455.feature b/CHANGES/2455.feature new file mode 100644 index 0000000000..b416e28bed --- /dev/null +++ b/CHANGES/2455.feature @@ -0,0 +1 @@ +Content-Disposition fast access in ClientResponse diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index 3f29c3670c..76c6c20b60 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -9,16 +9,17 @@ from collections import namedtuple from hashlib import md5, sha1, sha256 from http.cookies import CookieError, Morsel, SimpleCookie +from types import MappingProxyType from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy from yarl import URL -from . import hdrs, helpers, http, payload +from . import hdrs, helpers, http, multipart, payload from .client_exceptions import (ClientConnectionError, ClientOSError, ClientResponseError, ContentTypeError, InvalidURL) from .formdata import FormData -from .helpers import HeadersMixin, TimerNoop, noop +from .helpers import HeadersMixin, TimerNoop, noop, reify from .http import SERVER_SOFTWARE, HttpVersion10, HttpVersion11, PayloadWriter from .log import client_logger from .streams import FlowControlStreamReader @@ -33,6 +34,10 @@ __all__ = ('ClientRequest', 'ClientResponse', 'RequestInfo') +ContentDisposition = collections.namedtuple( + 'ContentDisposition', ('type', 'parameters', 'filename')) + + RequestInfo = collections.namedtuple( 'RequestInfo', ('url', 'method', 'headers')) @@ -527,6 +532,7 @@ def __init__(self, method, url, *, self._request_info = request_info self._timer = timer if timer is not None else TimerNoop() self._auto_decompress = auto_decompress + self._cache = {} # reqired for @reify method decorator @property def url(self): @@ -550,6 +556,16 @@ def _headers(self): def request_info(self): return self._request_info + @reify + def content_disposition(self): + raw = self._headers.get(hdrs.CONTENT_DISPOSITION) + if raw is None: + return None + disposition_type, params = multipart.parse_content_disposition(raw) + params = MappingProxyType(params) + filename = multipart.content_disposition_filename(params) + return ContentDisposition(disposition_type, params, filename) + def _post_init(self, loop, session): self._loop = loop self._session = session # store a reference to session #1985 diff --git a/docs/client_reference.rst b/docs/client_reference.rst index 151fa8db51..1737620ed8 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -1109,13 +1109,20 @@ Response object .. attribute:: charset - Read-only property that specifies the *encoding* for the request's BODY. + Read-only property that specifies the *encoding* for the request's BODY. The value is parsed from the *Content-Type* HTTP header. Returns :class:`str` like ``'utf-8'`` or ``None`` if no *Content-Type* header present in HTTP headers or it has no charset information. + .. attribute:: content_disposition + + Read-only property that specified the *Content-Disposition* HTTP header. + + Instance of :class:`ContentDisposition` or ``None`` if no *Content-Disposition* + header present in HTTP headers. + .. attribute:: history A :class:`~collections.abc.Sequence` of :class:`ClientResponse` @@ -1561,6 +1568,21 @@ All exceptions are available as members of *aiohttp* module. Invalid URL, :class:`yarl.URL` instance. +.. class:: ContentDisposition + + Represent Content-Disposition header + + .. attribute:: value + + A :class:`str` instance. Value of Content-Disposition header itself, e.g. ``attachment``. + + .. attribute:: filename + + A :class:`str` instance. Content filename extracted from parameters. May be ``None``. + + .. attribute:: parameters + + Read-only mapping contains all parameters. Response errors ^^^^^^^^^^^^^^^ diff --git a/tests/test_client_response.py b/tests/test_client_response.py index b0a0884574..4765265e05 100644 --- a/tests/test_client_response.py +++ b/tests/test_client_response.py @@ -458,6 +458,42 @@ def test_charset_no_charset(): assert response.charset is None +def test_content_disposition_full(): + response = ClientResponse('get', URL('http://def-cl-resp.org')) + response.headers = {'Content-Disposition': + 'attachment; filename="archive.tar.gz"; foo=bar'} + + assert 'attachment' == response.content_disposition.type + assert 'bar' == response.content_disposition.parameters["foo"] + assert 'archive.tar.gz' == response.content_disposition.filename + with pytest.raises(TypeError): + response.content_disposition.parameters["foo"] = "baz" + + +def test_content_disposition_no_parameters(): + response = ClientResponse('get', URL('http://def-cl-resp.org')) + response.headers = {'Content-Disposition': 'attachment'} + + assert 'attachment' == response.content_disposition.type + assert response.content_disposition.filename is None + assert {} == response.content_disposition.parameters + + +def test_content_disposition_no_header(): + response = ClientResponse('get', URL('http://def-cl-resp.org')) + response.headers = {} + + assert response.content_disposition is None + + +def test_content_disposition_cache(): + response = ClientResponse('get', URL('http://def-cl-resp.org')) + response.headers = {'Content-Disposition': 'attachment'} + cd = response.content_disposition + ClientResponse.headers = {'Content-Disposition': 'spam'} + assert cd is response.content_disposition + + def test_response_request_info(): url = 'http://def-cl-resp.org' headers = {'Content-Type': 'application/json;charset=cp1251'}