diff --git a/.gitignore b/.gitignore index 49e07472..968d26ff 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ pylint.txt docs/_build toy.py .gitignore +junit-report.xml \ No newline at end of file diff --git a/HISTORY.rst b/HISTORY.rst index 053d37a7..5f1b6a3a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,13 @@ History ------- +0.6.5 (2011-10-19) +++++++++++++++++++ + +* Offline (fast) test suite. +* Session dictionary argument merging. + + 0.6.4 (2011-10-13) ++++++++++++++++++ diff --git a/Makefile b/Makefile index 1686ea56..8b49b27e 100644 --- a/Makefile +++ b/Makefile @@ -2,10 +2,12 @@ init: pip install -r reqs.txt test: - nosetests test_requests.py --processes=30 + nosetests test_requests.py --with-color ci: init - nosetests --search-test --processes=30 --with-nosexunit test_requests.py + nosetests test_requests.py --with-xunit --xunit-file=junit-report.xml + +stats: pyflakes requests | awk -F\: '{printf "%s:%s: [E]%s\n", $1, $2, $3}' > violations.pyflakes.txt site: diff --git a/docs/community/out-there.rst b/docs/community/out-there.rst new file mode 100644 index 00000000..2e7b4715 --- /dev/null +++ b/docs/community/out-there.rst @@ -0,0 +1,63 @@ +Modules +======= + +- `requests-oauth-hook `_, adds OAuth support to Requests. +- `FacePy `_, a Python wrapper to the Facebook API. +- `robotframework-requests `_, a Robot Framework API wrapper. +- `fullerene `_, a Graphite Dashboard. +- `urbanairship-python `_, a fork of the Urban Airship API wrapper. + + +Articles & Talks +================ +- `Python for the Web `_ teaches how to use Python to interact with the web, using Requests. +- `Daniel Greenfield's Review of Requests `_ +- `My 'Python for Humans' talk `_ ( `audio `_ ) +- `Issac Kelly's 'Consuming Web APIs' talk `_ +- `Blog post about Requests via Yum `_ +- `Russian blog post introducing Requests `_ + + +Integrations +============ + +ScraperWiki +------------ + +`ScraperWiki `_ is an excellent service that allows +you to run Python, Ruby, and PHP scraper scripts on the web. Now, Requests +v0.6.1 is available to use in your scrapers! + +To give it a try, simply:: + + import requests + + +Managed Packages +================ + +Requests is available in a number of popular package formats. Of course, +the ideal way to install Requests is via The Cheeseshop. + + +Ubuntu & Debian +--------------- + +Requests is available installed as a Debian package! Debian Etch Ubuntu, since Oneiric:: + + $ apt-get install python-requests + +Unfortunately, the most recent version available is v0.5.0. If you're on the +Debian Python Package team, I'd love an update of that :) + + +Fedora and RedHat +----------------- + +You can easily install Requests v0.6.1 with yum on rpm-based systems:: + + $ yum install python-requests + + + + diff --git a/docs/index.rst b/docs/index.rst index d849ed5f..183d475a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -87,6 +87,7 @@ Requests ecosystem and community. :maxdepth: 2 community/faq + community/out-there.rst community/support community/updates diff --git a/reqs.txt b/reqs.txt new file mode 100644 index 00000000..46acb760 --- /dev/null +++ b/reqs.txt @@ -0,0 +1,7 @@ +envoy==0.0.2 +httpbin==0.0.4 +gunicorn +nose +pyflakes +omnijson +rudolf \ No newline at end of file diff --git a/requests/core.py b/requests/core.py index de05cf9f..9e518357 100644 --- a/requests/core.py +++ b/requests/core.py @@ -12,8 +12,8 @@ This module implements the main Requests system. """ __title__ = 'requests' -__version__ = '0.6.4' -__build__ = 0x000604 +__version__ = '0.6.5' +__build__ = 0x000605 __author__ = 'Kenneth Reitz' __license__ = 'ISC' __copyright__ = 'Copyright 2011 Kenneth Reitz' diff --git a/requests/sessions.py b/requests/sessions.py index 50b09f61..4e7022d6 100644 --- a/requests/sessions.py +++ b/requests/sessions.py @@ -16,21 +16,60 @@ from .utils import add_dict_to_cookiejar +def merge_kwargs(local_kwarg, default_kwarg): + """Merges kwarg dictionaries. + + If a local key in the dictionary is set to None, it will be removed. + """ + + if default_kwarg is None: + return local_kwarg + + if local_kwarg is None: + return default_kwarg + + # Bypass if not a dictionary (e.g. timeout) + if not hasattr(default_kwarg, 'items'): + return local_kwarg + + + + # Update new values. + kwargs = default_kwarg.copy() + kwargs.update(local_kwarg) + + # Remove keys that are set to None. + for (k,v) in local_kwarg.items(): + if v is None: + del kwargs[k] + + return kwargs + + class Session(object): """A Requests session.""" __attrs__ = ['headers', 'cookies', 'auth', 'timeout', 'proxies', 'hooks'] - def __init__(self, **kwargs): + def __init__(self, + headers=None, + cookies=None, + auth=None, + timeout=None, + proxies=None, + hooks=None): + + self.headers = headers or {} + self.cookies = cookies or {} + self.auth = auth + self.timeout = timeout + self.proxies = proxies or {} + self.hooks = hooks or {} # Set up a CookieJar to be used by default self.cookies = cookielib.FileCookieJar() - # Map args from kwargs to instance-local variables - map(lambda k, v: (k in self.__attrs__) and setattr(self, k, v), - kwargs.iterkeys(), kwargs.itervalues()) - # Map and wrap requests.api methods self._map_api_methods() @@ -42,10 +81,8 @@ class Session(object): return self def __exit__(self, *args): - # print args pass - def _map_api_methods(self): """Reads each available method from requests.api and decorates them with a wrapper, which inserts any instance-local attributes @@ -54,23 +91,35 @@ class Session(object): def pass_args(func): def wrapper_func(*args, **kwargs): - inst_attrs = dict((k, v) for k, v in self.__dict__.iteritems() - if k in self.__attrs__) - # Combine instance-local values with kwargs values, with - # priority to values in kwargs - kwargs = dict(inst_attrs.items() + kwargs.items()) + + # Argument collector. + _kwargs = {} # If a session request has a cookie_dict, inject the # values into the existing CookieJar instead. if isinstance(kwargs.get('cookies', None), dict): kwargs['cookies'] = add_dict_to_cookiejar( - inst_attrs['cookies'], kwargs['cookies'] + self.cookies, kwargs['cookies'] ) - if kwargs.get('headers', None) and inst_attrs.get('headers', None): - kwargs['headers'].update(inst_attrs['headers']) + for attr in self.__attrs__: + # for attr in ['headers',]: + s_val = self.__dict__.get(attr) + r_val = kwargs.get(attr) + + new_attr = merge_kwargs(r_val, s_val) + + # Skip attributes that were set to None. + if new_attr is not None: + _kwargs[attr] = new_attr + + # Make sure we didn't miss anything. + for (k, v) in kwargs.items(): + if k not in _kwargs: + _kwargs[k] = v + + return func(*args, **_kwargs) - return func(*args, **kwargs) return wrapper_func # Map and decorate each function available in requests.api diff --git a/test_requests.py b/test_requests.py index 83b827ab..dbfe527d 100755 --- a/test_requests.py +++ b/test_requests.py @@ -3,8 +3,12 @@ from __future__ import with_statement -import unittest +import time import cookielib +import os +import unittest + +import envoy try: import omnijson as json @@ -15,12 +19,10 @@ import requests from requests.sessions import Session +PORT = os.environ.get('HTTPBIN_PORT', '7045') -HTTPBIN_URL = 'http://httpbin.ep.io/' -HTTPSBIN_URL = 'https://httpbin.ep.io/' - -# HTTPBIN_URL = 'http://staging.httpbin.org/' -# HTTPSBIN_URL = 'https://httpbin-staging.ep.io/' +HTTPBIN_URL = 'http://0.0.0.0:%s/' % (PORT) +# HTTPBIN_URL = 'http://127.0.0.1:8000/' def httpbin(*suffix): @@ -29,15 +31,9 @@ def httpbin(*suffix): return HTTPBIN_URL + '/'.join(suffix) -def httpsbin(*suffix): - """Returns url for HTTPSBIN resource.""" - - return HTTPSBIN_URL + '/'.join(suffix) - - -SERVICES = (httpbin, httpsbin) - +SERVICES = (httpbin, ) +_httpbin = False class RequestsTestSuite(unittest.TestCase): """Requests test cases.""" @@ -46,12 +42,21 @@ class RequestsTestSuite(unittest.TestCase): _multiprocess_can_split_ = True def setUp(self): - pass + + global _httpbin + + if not _httpbin: + + self.httpbin = envoy.connect('gunicorn httpbin:app --bind=0.0.0.0:%s' % (PORT)) + + _httpbin = True + time.sleep(1) + def tearDown(self): """Teardown.""" - pass + # self.httpbin.kill() def test_invalid_url(self): @@ -59,7 +64,7 @@ class RequestsTestSuite(unittest.TestCase): def test_HTTP_200_OK_GET(self): - r = requests.get(httpbin('/')) + r = requests.get(httpbin('/get')) self.assertEqual(r.status_code, 200) def test_HTTP_302_ALLOW_REDIRECT_GET(self): @@ -70,10 +75,6 @@ class RequestsTestSuite(unittest.TestCase): r = requests.get(httpbin('redirect', '1'), allow_redirects=False) self.assertEqual(r.status_code, 302) - def test_HTTPS_200_OK_GET(self): - r = requests.get(httpsbin('/')) - self.assertEqual(r.status_code, 200) - def test_HTTP_200_OK_GET_WITH_PARAMS(self): heads = {'User-agent': 'Mozilla/5.0'} @@ -112,12 +113,7 @@ class RequestsTestSuite(unittest.TestCase): def test_HTTP_200_OK_HEAD(self): - r = requests.head(httpbin('/')) - self.assertEqual(r.status_code, 200) - - - def test_HTTPS_200_OK_HEAD(self): - r = requests.head(httpsbin('/')) + r = requests.head(httpbin('/get')) self.assertEqual(r.status_code, 200) @@ -126,21 +122,11 @@ class RequestsTestSuite(unittest.TestCase): self.assertEqual(r.status_code, 200) - def test_HTTPS_200_OK_PUT(self): - r = requests.put(httpsbin('put')) - self.assertEqual(r.status_code, 200) - - def test_HTTP_200_OK_PATCH(self): r = requests.patch(httpbin('patch')) self.assertEqual(r.status_code, 200) - def test_HTTPS_200_OK_PATCH(self): - r = requests.patch(httpsbin('patch')) - self.assertEqual(r.status_code, 200) - - def test_AUTH_HTTP_200_OK_GET(self): for service in SERVICES: @@ -206,7 +192,7 @@ class RequestsTestSuite(unittest.TestCase): r = requests.get(service('status', '500')) self.assertEqual(bool(r), False) - r = requests.get(service('/')) + r = requests.get(service('/get')) self.assertEqual(bool(r), True) @@ -257,7 +243,7 @@ class RequestsTestSuite(unittest.TestCase): for service in SERVICES: - url = service('/') + url = service('/get') requests.get(url, params={'foo': u'føø'}) requests.get(url, params={u'føø': u'føø'}) @@ -275,19 +261,6 @@ class RequestsTestSuite(unittest.TestCase): self.assertEquals(r.status_code, 401) - def test_settings(self): - - def test(): - r = requests.get(httpbin('')) - r.raise_for_status() - - with requests.settings(timeout=0.0000001): - self.assertRaises(requests.Timeout, test) - - with requests.settings(timeout=100): - requests.get(httpbin('')) - - def test_urlencoded_post_data(self): for service in SERVICES: @@ -386,9 +359,9 @@ class RequestsTestSuite(unittest.TestCase): self.assertEquals(rbody.get('data'), 'foobar') - def test_idna(self): - r = requests.get(u'http://➡.ws/httpbin') - assert 'httpbin' in r.url + # def test_idna(self): + # r = requests.get(u'http://➡.ws/httpbin') + # assert 'httpbin' in r.url def test_urlencoded_get_query_multivalued_param(self): @@ -470,14 +443,7 @@ class RequestsTestSuite(unittest.TestCase): def test_session_HTTP_200_OK_GET(self): s = Session() - r = s.get(httpbin('/')) - self.assertEqual(r.status_code, 200) - - - def test_session_HTTPS_200_OK_GET(self): - - s = Session() - r = s.get(httpsbin('/')) + r = s.get(httpbin('/get')) self.assertEqual(r.status_code, 200) @@ -487,13 +453,18 @@ class RequestsTestSuite(unittest.TestCase): s = Session() s.headers = heads + # Make 2 requests from Session object, should send header both times r1 = s.get(httpbin('user-agent')) - assert heads['User-agent'] in r1.content - r2 = s.get(httpbin('user-agent')) + r2 = s.get(httpbin('user-agent')) assert heads['User-agent'] in r2.content + + new_heads = {'User-agent': 'blah'} + r3 = s.get(httpbin('user-agent'), headers=new_heads) + assert new_heads['User-agent'] in r3.content + self.assertEqual(r2.status_code, 200)