Skip to content

Commit

Permalink
Add 'content_type' & 'charset' properties to aiohttp.ClientResponse (#…
Browse files Browse the repository at this point in the history
…1352)

* Add 'content_type' & 'charset' properties to aiohttp.ClientResponse

* Inherit ClientResponse from HeadersMixin.
Change `content_type` property behavior if no Content-Type header.

* Fix imports order
  • Loading branch information
moden-py authored and asvetlov committed Oct 31, 2016
1 parent 71a22e6 commit 9cecc65
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 46 deletions.
3 changes: 2 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ CHANGES

- Boost performance by adding a custom time service #1350

-
- Extend `ClientResponse` with `content_type` and `charset`
properties like in `web.Request`. #1349

-

Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Daniel García
Daniel Nelson
Danny Song
David Michael Brown
Denis Matiychuk
Dima Veselov
Dimitar Dimitrov
Dmitry Shamov
Expand Down
6 changes: 3 additions & 3 deletions aiohttp/client_reqrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import aiohttp

from . import hdrs, helpers, streams
from .helpers import Timeout
from .helpers import HeadersMixin, Timeout
from .log import client_logger
from .multipart import MultipartWriter
from .protocol import HttpMessage
Expand Down Expand Up @@ -488,7 +488,7 @@ def terminate(self):
self._writer = None


class ClientResponse:
class ClientResponse(HeadersMixin):

# from the Status-Line of the response
version = None # HTTP-Version
Expand Down Expand Up @@ -580,7 +580,7 @@ def connection(self):

@property
def history(self):
"""A sequence of of responses, if redirects occured."""
"""A sequence of of responses, if redirects occurred."""
return self._history

def waiting_for_continue(self):
Expand Down
42 changes: 42 additions & 0 deletions aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import asyncio
import base64
import binascii
import cgi
import datetime
import functools
import io
Expand Down Expand Up @@ -620,3 +621,44 @@ def strtime(self):
if s is None:
self._strtime = s = self._format_date_time()
return self._strtime


class HeadersMixin:

_content_type = None
_content_dict = None
_stored_content_type = sentinel

def _parse_content_type(self, raw):
self._stored_content_type = raw
if raw is None:
# default value according to RFC 2616
self._content_type = 'application/octet-stream'
self._content_dict = {}
else:
self._content_type, self._content_dict = cgi.parse_header(raw)

@property
def content_type(self, _CONTENT_TYPE=hdrs.CONTENT_TYPE):
"""The value of content part for Content-Type HTTP header."""
raw = self.headers.get(_CONTENT_TYPE)
if self._stored_content_type != raw:
self._parse_content_type(raw)
return self._content_type

@property
def charset(self, _CONTENT_TYPE=hdrs.CONTENT_TYPE):
"""The value of charset part for Content-Type HTTP header."""
raw = self.headers.get(_CONTENT_TYPE)
if self._stored_content_type != raw:
self._parse_content_type(raw)
return self._content_dict.get('charset')

@property
def content_length(self, _CONTENT_LENGTH=hdrs.CONTENT_LENGTH):
"""The value of Content-Length HTTP header."""
l = self.headers.get(_CONTENT_LENGTH)
if l is None:
return None
else:
return int(l)
43 changes: 1 addition & 42 deletions aiohttp/web_reqrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from yarl import URL

from . import hdrs, multipart
from .helpers import reify, sentinel
from .helpers import HeadersMixin, reify, sentinel
from .protocol import WebResponse as ResponseImpl
from .protocol import HttpVersion10, HttpVersion11
from .streams import EOF_MARKER
Expand All @@ -27,47 +27,6 @@
'json_response'
)


class HeadersMixin:

_content_type = None
_content_dict = None
_stored_content_type = sentinel

def _parse_content_type(self, raw):
self._stored_content_type = raw
if raw is None:
# default value according to RFC 2616
self._content_type = 'application/octet-stream'
self._content_dict = {}
else:
self._content_type, self._content_dict = cgi.parse_header(raw)

@property
def content_type(self, _CONTENT_TYPE=hdrs.CONTENT_TYPE):
"""The value of content part for Content-Type HTTP header."""
raw = self.headers.get(_CONTENT_TYPE)
if self._stored_content_type != raw:
self._parse_content_type(raw)
return self._content_type

@property
def charset(self, _CONTENT_TYPE=hdrs.CONTENT_TYPE):
"""The value of charset part for Content-Type HTTP header."""
raw = self.headers.get(_CONTENT_TYPE)
if self._stored_content_type != raw:
self._parse_content_type(raw)
return self._content_dict.get('charset')

@property
def content_length(self, _CONTENT_LENGTH=hdrs.CONTENT_LENGTH):
"""The value of Content-Length HTTP header."""
l = self.headers.get(_CONTENT_LENGTH)
if l is None:
return None
else:
return int(l)

FileField = collections.namedtuple('Field', 'name filename file content_type')


Expand Down
21 changes: 21 additions & 0 deletions docs/client_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,27 @@ Response object
HTTP headers of response as unconverted bytes, a sequence of
``(key, value)`` pairs.

.. attribute:: content_type

Read-only property with *content* part of *Content-Type* header.

.. note::

Returns value is ``'application/octet-stream'`` if no
Content-Type header present in HTTP headers according to
:rfc:`2616`. To make sure Content-Type header is not present in
the server reply, use :attr:`headers` or :attr:`raw_headers`, e.g.
``'CONTENT-TYPE' not in resp.headers``.

.. attribute:: charset

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:: history

A :class:`~collections.abc.Sequence` of :class:`ClientResponse`
Expand Down
35 changes: 35 additions & 0 deletions tests/test_client_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,38 @@ def test_resp_host():
response = ClientResponse('get', URL('http://del-cl-resp.org'))
with pytest.warns(DeprecationWarning):
assert 'del-cl-resp.org' == response.host


def test_content_type():
response = ClientResponse('get', URL('http://def-cl-resp.org'))
response.headers = {'Content-Type': 'application/json;charset=cp1251'}

assert 'application/json' == response.content_type


def test_content_type_no_header():
response = ClientResponse('get', URL('http://def-cl-resp.org'))
response.headers = {}

assert 'application/octet-stream' == response.content_type


def test_charset():
response = ClientResponse('get', URL('http://def-cl-resp.org'))
response.headers = {'Content-Type': 'application/json;charset=cp1251'}

assert 'cp1251' == response.charset


def test_charset_no_header():
response = ClientResponse('get', URL('http://def-cl-resp.org'))
response.headers = {}

assert response.charset is None


def test_charset_no_charset():
response = ClientResponse('get', URL('http://def-cl-resp.org'))
response.headers = {'Content-Type': 'application/json'}

assert response.charset is None

0 comments on commit 9cecc65

Please sign in to comment.