diff --git a/CHANGES/2455.feature b/CHANGES/2455.feature new file mode 100644 index 00000000000..b416e28bedb --- /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 3f29c3670c2..4cb9ceff3e0 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -7,18 +7,19 @@ import traceback import warnings from collections import namedtuple +from types import MappingProxyType from hashlib import md5, sha1, sha256 from http.cookies import CookieError, Morsel, SimpleCookie from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy from yarl import URL -from . import hdrs, helpers, http, payload +from . import hdrs, helpers, http, payload, multipart 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', ('value', '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 = {} @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 + value, params = multipart.parse_content_disposition(raw) + params = MappingProxyType(params) + filename = multipart.content_disposition_filename(params) + return ContentDisposition(value, 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 101aa3163d7..ad81720f595 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 + + Value of Content-Disposition header itself, e.g. ``attachment``. + + .. attribute:: filename + + Content filename extracted from parameters. May be ``None``. + + .. attribute:: parameters + + A :class:`dict` instance contains all parameters. Response errors ^^^^^^^^^^^^^^^ diff --git a/tests/test_client_response.py b/tests/test_client_response.py index b0a08845747..4aeb413d5d5 100644 --- a/tests/test_client_response.py +++ b/tests/test_client_response.py @@ -458,6 +458,32 @@ 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.value + assert 'bar' == response.content_disposition.parameters["foo"] + assert 'archive.tar.gz' == response.content_disposition.filename + + +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.value + 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_response_request_info(): url = 'http://def-cl-resp.org' headers = {'Content-Type': 'application/json;charset=cp1251'}