Allow POST and PUT requests to take both querystring params and request body
authorRichard Boulton <richard@tartarus.org>
Thu, 16 Jun 2011 14:48:43 +0000 (15:48 +0100)
committerRichard Boulton <richard@tartarus.org>
Thu, 16 Jun 2011 14:48:43 +0000 (15:48 +0100)
data.

requests/api.py
requests/models.py
test_requests.py

index c3c211c1cef3574771ec8ee9c5ab7c25c37a0c0d..cf2e575e615d150d5954dfd35af01333ac07c9da 100644 (file)
@@ -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):
index 23555b6760a8f81af1ee81d2db79b69619c35dbf..6f19718c5030d339f21578b003833593943f8e97 100644 (file)
@@ -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 @@ class Request(object):
         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 <models.Request>`.
-        self.data = dict()
+        #: Dictionary or byte of request body data to attach to the
+        #: :class:`Request <models.Request>`.
+        self.data = None
+        #: Dictionary or byte of querystring data to attach to the
+        #: :class:`Request <models.Request>`.
+        self.params = None
         #: True if :class:`Request <models.Request>` is part of a redirect chain (disables history
         #: and HTTPError storage).
         self.redirect = redirect
@@ -53,6 +57,8 @@ class Request(object):
         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 <models.Response>` instance, containing
         #: content and metadata of HTTP Response, once :attr:`sent <send>`.
         self.response = Response()
@@ -185,7 +191,8 @@ class Request(object):
 
                 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 @@ class Request(object):
             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 @@ class Request(object):
         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 @@ class Request(object):
                     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)
index 6aaf0b9aac64642ac7591f5d4570f8bf183c0aa4..b1fe22cdf285f7294bc53ebaaec0af08189c4486 100755 (executable)
@@ -5,6 +5,10 @@ from __future__ import with_statement
 
 import unittest
 import cookielib
+try:
+    import simplejson as json
+except ImportError:
+    import json
 
 import requests
 
@@ -229,8 +233,68 @@ class RequestsTestSuite(unittest.TestCase):
             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')