Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions requests/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def request(method, url, **kwargs):
:param url: URL for the new :class:`Request` object.
:param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`.
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
:param json: (optional) json data to send in the body of the :class:`Request`.
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
:param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
:param files: (optional) Dictionary of 'name': file-like-objects (or {'name': ('filename', fileobj)}) for multipart encoding upload.
Expand Down Expand Up @@ -77,15 +78,16 @@ def head(url, **kwargs):
return request('head', url, **kwargs)


def post(url, data=None, **kwargs):
def post(url, data=None, json=None, **kwargs):
"""Sends a POST request. Returns :class:`Response` object.

:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
:param json: (optional) json data to send in the body of the :class:`Request`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would get passed through with the kwargs, right, so I guess this is just so people know about the parameter?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:param \*\*kwargs: Optional arguments that ``request`` takes.
"""

return request('post', url, data=data, **kwargs)
return request('post', url, data=data, json=json, **kwargs)


def put(url, data=None, **kwargs):
Expand Down
17 changes: 13 additions & 4 deletions requests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ class Request(RequestHooksMixin):
:param headers: dictionary of headers to send.
:param files: dictionary of {filename: fileobject} files to multipart upload.
:param data: the body to attach the request. If a dictionary is provided, form-encoding will take place.
:param json: json for the body to attach the request.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/attach the request/attach to the request :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

:param params: dictionary of URL parameters to append to the URL.
:param auth: Auth handler or (user, pass) tuple.
:param cookies: dictionary or CookieJar of cookies to attach to this request.
Expand All @@ -209,13 +210,15 @@ def __init__(self,
headers=None,
files=None,
data=None,
json=None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I'm late to the party, was out - wouldn't this also change the meaning for anyone who creates a Request by hand, eg

params = {'date_created': '1-1-14'}
req = Request('GET', 'http://jsonip.com', {'User-Agent': 'foobar'}, None, {}, params)

where now suddenly params is JSON data?

params=None,
auth=None,
cookies=None,
hooks=None):

# Default empty dicts for dict params.
data = [] if data is None else data
json = [] if json is None else json
files = [] if files is None else files
headers = {} if headers is None else headers
params = {} if params is None else params
Expand All @@ -230,6 +233,7 @@ def __init__(self,
self.headers = headers
self.files = files
self.data = data
self.json = json
self.params = params
self.auth = auth
self.cookies = cookies
Expand All @@ -246,6 +250,7 @@ def prepare(self):
headers=self.headers,
files=self.files,
data=self.data,
json=self.json,
params=self.params,
auth=self.auth,
cookies=self.cookies,
Expand Down Expand Up @@ -289,14 +294,14 @@ def __init__(self):
self.hooks = default_hooks()

def prepare(self, method=None, url=None, headers=None, files=None,
data=None, params=None, auth=None, cookies=None, hooks=None):
data=None, json=None, params=None, auth=None, cookies=None, hooks=None):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the event that someone is calling prepare with all of its arguments and no keyword arguments, won't this suddenly change their params argument to now be the json argument?

Eg

request.prepare('get', '/foo', {}, {}, {}, {'foo': 'bar'}, {'auth': 'pass'})

Everything gets shifted by one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. This parameter should be moved to the end since we'll be invoking it as a keyword argument anyway.

One more reason I wish we were using Python 3, because then we could make these all keyword-only arguments or only some of them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not to say this is invalid, but I wonder how many people call prepare on a PreparedRequest.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suspect quite a few. The real question is how many do it with positional arguments only. Regardless, let's not offer them a footgun.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

"""Prepares the entire request with the given parameters."""

self.prepare_method(method)
self.prepare_url(url, params)
self.prepare_headers(headers)
self.prepare_cookies(cookies)
self.prepare_body(data, files)
self.prepare_body(data, files, json)
self.prepare_auth(auth, url)
# Note that prepare_auth must be last to enable authentication schemes
# such as OAuth to work on a fully prepared request.
Expand Down Expand Up @@ -397,7 +402,7 @@ def prepare_headers(self, headers):
else:
self.headers = CaseInsensitiveDict()

def prepare_body(self, data, files):
def prepare_body(self, data, files, _json=None):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the underscore?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working on changing that already. The problem is that the module json is imported at the top and needs to be used.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by aliasing json_dumps = json.dumps at the top of the module. Not sure I like it, but apparently import semantics are such that requests.compat.json can't be imported from (e.g., you can't do from .compat.json import dumps). Can you think of a better solution to avoid the local variable/parameter from shadowing the top-level module name beyond doing from . import compat and then doing compat.json.dumps?

"""Prepares the given HTTP body data."""

# Check if file, fo, generator, iterator.
Expand All @@ -408,6 +413,10 @@ def prepare_body(self, data, files):
content_type = None
length = None

if _json is not None:
content_type = 'application/json'
data = json.dumps(_json)

is_stream = all([
hasattr(data, '__iter__'),
not isinstance(data, (basestring, list, tuple, dict))
Expand All @@ -433,7 +442,7 @@ def prepare_body(self, data, files):
if files:
(body, content_type) = self._encode_files(files, data)
else:
if data:
if data and _json is None:
body = self._encode_params(data)
if isinstance(data, basestring) or hasattr(data, 'read'):
content_type = None
Expand Down
10 changes: 8 additions & 2 deletions requests/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ def prepare_request(self, request):
url=request.url,
files=request.files,
data=request.data,
json=request.json,
headers=merge_setting(request.headers, self.headers, dict_class=CaseInsensitiveDict),
params=merge_setting(request.params, self.params),
auth=merge_setting(auth, self.auth),
Expand All @@ -376,6 +377,7 @@ def prepare_request(self, request):
def request(self, method, url,
params=None,
data=None,
json=None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here.

headers=None,
cookies=None,
files=None,
Expand All @@ -396,6 +398,8 @@ def request(self, method, url,
string for the :class:`Request`.
:param data: (optional) Dictionary or bytes to send in the body of the
:class:`Request`.
:param json: (optional) json to send in the body of the
:class:`Request`.
:param headers: (optional) Dictionary of HTTP Headers to send with the
:class:`Request`.
:param cookies: (optional) Dict or CookieJar object to send with the
Expand Down Expand Up @@ -426,6 +430,7 @@ def request(self, method, url,
headers = headers,
files = files,
data = data or {},
json = json or {},
params = params or {},
auth = auth,
cookies = cookies,
Expand Down Expand Up @@ -479,15 +484,16 @@ def head(self, url, **kwargs):
kwargs.setdefault('allow_redirects', False)
return self.request('HEAD', url, **kwargs)

def post(self, url, data=None, **kwargs):
def post(self, url, data=None, json=None, **kwargs):
"""Sends a POST request. Returns :class:`Response` object.

:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
:param json: (optional) json to send in the body of the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes.
"""

return self.request('POST', url, data=data, **kwargs)
return self.request('POST', url, data=data, json=json, **kwargs)

def put(self, url, data=None, **kwargs):
"""Sends a PUT request. Returns :class:`Response` object.
Expand Down
9 changes: 9 additions & 0 deletions test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,15 @@ def test_requests_history_is_saved(self):
assert item.history == total[0:i]
i=i+1

def test_json_param_post_content_type_works(self):
r = requests.post(
httpbin('post'),
json={'life': 42}
)
assert r.status_code == 200
assert 'application/json' in r.request.headers['Content-Type']
assert {'life': 42} == r.json()['json']


class TestContentEncodingDetection(unittest.TestCase):

Expand Down