From 80d860d2d409b6d2a6b4c133ef0eef4de6b32ef2 Mon Sep 17 00:00:00 2001 From: Richard Boulton Date: Thu, 16 Jun 2011 15:48:43 +0100 Subject: [PATCH] Allow POST and PUT requests to take both querystring params and request body data. --- requests/api.py | 24 +++++++++-------- requests/models.py | 41 +++++++++++++++++------------ test_requests.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 27 deletions(-) diff --git a/requests/api.py b/requests/api.py index c3c211c1ce..cf2e575e61 100644 --- a/requests/api.py +++ b/requests/api.py @@ -34,13 +34,11 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, fil :param allow_redirects: (optional) Boolean. Set to True if POST/PUT/DELETE redirect following is allowed. """ - if params and data: - raise StandardError('You may provide either params or data to a request, but not both.') - r = Request( method = method, url = url, - data = params or data, + data = data, + params = params, headers = headers, cookiejar = cookies, files = files, @@ -81,7 +79,8 @@ def head(url, params=None, headers=None, cookies=None, auth=None, timeout=None): return request('HEAD', url, params=params, headers=headers, cookies=cookies, auth=auth, timeout=timeout) -def post(url, data='', headers=None, files=None, cookies=None, auth=None, timeout=None, allow_redirects=False): +def post(url, data='', headers=None, files=None, cookies=None, auth=None, + timeout=None, allow_redirects=False, params=None): """Sends a POST request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. @@ -92,13 +91,16 @@ def post(url, data='', headers=None, files=None, cookies=None, auth=None, timeou :param auth: (optional) AuthObject to enable Basic HTTP Auth. :param timeout: (optional) Float describing the timeout of the request. :param allow_redirects: (optional) Boolean. Set to True if redirect following is allowed. + :param params: (optional) Dictionary of parameters, or bytes, to be sent in the query string for the :class:`Request`. """ - return request('POST', url, data=data, headers=headers, files=files, cookies=cookies, auth=auth, - timeout=timeout, allow_redirects=allow_redirects) + return request('POST', url, params=params, data=data, headers=headers, + files=files, cookies=cookies, auth=auth, timeout=timeout, + allow_redirects=allow_redirects) -def put(url, data='', headers=None, files=None, cookies=None, auth=None, timeout=None, allow_redirects=False): +def put(url, data='', headers=None, files=None, cookies=None, auth=None, + timeout=None, allow_redirects=False, params=None): """Sends a PUT request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. @@ -109,10 +111,12 @@ def put(url, data='', headers=None, files=None, cookies=None, auth=None, timeout :param auth: (optional) AuthObject to enable Basic HTTP Auth. :param timeout: (optional) Float describing the timeout of the request. :param allow_redirects: (optional) Boolean. Set to True if redirect following is allowed. + :param params: (optional) Dictionary of parameters, or bytes, to be sent in the query string for the :class:`Request`. """ - return request('PUT', url, data=data, headers=headers, files=files, cookies=cookies, auth=auth, - timeout=timeout, allow_redirects=allow_redirects) + return request('PUT', url, params=params, data=data, headers=headers, + files=files, cookies=cookies, auth=auth, timeout=timeout, + allow_redirects=allow_redirects) def delete(url, params=None, headers=None, cookies=None, auth=None, timeout=None, allow_redirects=False): diff --git a/requests/models.py b/requests/models.py index 23555b6760..6f19718c50 100644 --- a/requests/models.py +++ b/requests/models.py @@ -31,8 +31,8 @@ class Request(object): _METHODS = ('GET', 'HEAD', 'PUT', 'POST', 'DELETE') def __init__(self, url=None, headers=dict(), files=None, method=None, - data=dict(), auth=None, cookiejar=None, timeout=None, - redirect=False, allow_redirects=False): + data=dict(), params=dict(), auth=None, cookiejar=None, + timeout=None, redirect=False, allow_redirects=False): socket.setdefaulttimeout(timeout) @@ -44,8 +44,12 @@ def __init__(self, url=None, headers=dict(), files=None, method=None, self.files = files #: HTTP Method to use. Available: GET, HEAD, PUT, POST, DELETE. self.method = method - #: Form or Byte data to attach to the :class:`Request `. - self.data = dict() + #: Dictionary or byte of request body data to attach to the + #: :class:`Request `. + self.data = None + #: Dictionary or byte of querystring data to attach to the + #: :class:`Request `. + self.params = None #: True if :class:`Request ` is part of a redirect chain (disables history #: and HTTPError storage). self.redirect = redirect @@ -53,6 +57,8 @@ def __init__(self, url=None, headers=dict(), files=None, method=None, self.allow_redirects = allow_redirects self.data, self._enc_data = self._encode_params(data) + self.params, self._enc_params = self._encode_params(params) + #: :class:`Response ` instance, containing #: content and metadata of HTTP Response, once :attr:`sent `. self.response = Response() @@ -185,7 +191,8 @@ def build(resp): request = Request( url, self.headers, self.files, method, - self.data, self.auth, self.cookiejar, redirect=False + self.data, self.params, self.auth, self.cookiejar, + redirect=False ) request.send() r = request.response @@ -217,17 +224,16 @@ def _encode_params(data): return data, data - @staticmethod - def _build_url(url, data=None): - """Build URLs.""" + def _build_url(self): + """Build the actual URL to use""" - if urlparse(url).query: - return '%s&%s' % (url, data) - else: - if data: - return '%s?%s' % (url, data) + if self._enc_params: + if urlparse(self.url).query: + return '%s&%s' % (self.url, self._enc_params) else: - return url + return '%s?%s' % (self.url, self._enc_params) + else: + return self.url def send(self, anyway=False): @@ -243,8 +249,9 @@ def send(self, anyway=False): self._checks() success = False + url = self._build_url() if self.method in ('GET', 'HEAD', 'DELETE'): - req = _Request(self._build_url(self.url, self._enc_data), method=self.method) + req = _Request(url, method=self.method) else: if self.files: @@ -254,10 +261,10 @@ def send(self, anyway=False): self.files.update(self.data) datagen, headers = multipart_encode(self.files) - req = _Request(self.url, data=datagen, headers=headers, method=self.method) + req = _Request(url, data=datagen, headers=headers, method=self.method) else: - req = _Request(self.url, data=self._enc_data, method=self.method) + req = _Request(url, data=self._enc_data, method=self.method) if self.headers: req.headers.update(self.headers) diff --git a/test_requests.py b/test_requests.py index 6aaf0b9aac..b1fe22cdf2 100755 --- a/test_requests.py +++ b/test_requests.py @@ -5,6 +5,10 @@ import unittest import cookielib +try: + import simplejson as json +except ImportError: + import json import requests @@ -229,8 +233,68 @@ def test_settings(self): requests.get(httpbin('')) + def test_urlencoded_post_data(self): + r = requests.post(httpbin('post'), data=dict(test='fooaowpeuf')) + self.assertEquals(r.status_code, 200) + self.assertEquals(r.headers['content-type'], 'application/json') + self.assertEquals(r.url, httpbin('post')) + rbody = json.loads(r.content) + self.assertEquals(rbody.get('form'), dict(test='fooaowpeuf')) + self.assertEquals(rbody.get('data'), '') + + def test_nonurlencoded_post_data(self): r = requests.post(httpbin('post'), data='fooaowpeuf') + self.assertEquals(r.status_code, 200) + self.assertEquals(r.headers['content-type'], 'application/json') + self.assertEquals(r.url, httpbin('post')) + rbody = json.loads(r.content) + # Body wasn't valid url encoded data, so the server returns None as + # "form" and the raw body as "data". + self.assertEquals(rbody.get('form'), None) + self.assertEquals(rbody.get('data'), 'fooaowpeuf') + + + def test_urlencoded_post_querystring(self): + r = requests.post(httpbin('post'), params=dict(test='fooaowpeuf')) + self.assertEquals(r.status_code, 200) + self.assertEquals(r.headers['content-type'], 'application/json') + self.assertEquals(r.url, httpbin('post?test=fooaowpeuf')) + rbody = json.loads(r.content) + self.assertEquals(rbody.get('form'), {}) # No form supplied + self.assertEquals(rbody.get('data'), '') + + + def test_nonurlencoded_post_querystring(self): + r = requests.post(httpbin('post'), params='fooaowpeuf') + self.assertEquals(r.status_code, 200) + self.assertEquals(r.headers['content-type'], 'application/json') + self.assertEquals(r.url, httpbin('post?fooaowpeuf')) + rbody = json.loads(r.content) + self.assertEquals(rbody.get('form'), {}) # No form supplied + self.assertEquals(rbody.get('data'), '') + + + def test_urlencoded_post_query_and_data(self): + r = requests.post(httpbin('post'), params=dict(test='fooaowpeuf'), + data=dict(test2="foobar")) + self.assertEquals(r.status_code, 200) + self.assertEquals(r.headers['content-type'], 'application/json') + self.assertEquals(r.url, httpbin('post?test=fooaowpeuf')) + rbody = json.loads(r.content) + self.assertEquals(rbody.get('form'), dict(test2='foobar')) + self.assertEquals(rbody.get('data'), '') + + + def test_nonurlencoded_post_query_and_data(self): + r = requests.post(httpbin('post'), params='fooaowpeuf', + data="foobar") + self.assertEquals(r.status_code, 200) + self.assertEquals(r.headers['content-type'], 'application/json') + self.assertEquals(r.url, httpbin('post?fooaowpeuf')) + rbody = json.loads(r.content) + self.assertEquals(rbody.get('form'), None) + self.assertEquals(rbody.get('data'), 'foobar')