Skip to content

Commit fee85cf

Browse files
committed
Raise exception if json is not the expected type
There are a few corner cases we need to handle where json isn't the typical type we expect. In those (rare) cases, we need to raise a better exception for the user's benefit. Closes sigmavirus24#310
1 parent 28f2db8 commit fee85cf

File tree

6 files changed

+54
-32
lines changed

6 files changed

+54
-32
lines changed

github3/exceptions.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44

55
class GitHubError(Exception):
6-
76
"""The base exception class."""
87

98
def __init__(self, resp):
@@ -35,76 +34,70 @@ def message(self):
3534
return self.msg
3635

3736

38-
class BadRequest(GitHubError):
37+
class UnprocessableResponseBody(GitHubError):
38+
"""Exception class for response objects that cannot be handled."""
39+
def __init__(self, message, body):
40+
Exception.__init__(self, message)
41+
self.body = body
42+
43+
def __str__(self):
44+
return self.message
3945

40-
"""Exception class for 400 responses."""
4146

47+
class BadRequest(GitHubError):
48+
"""Exception class for 400 responses."""
4249
pass
4350

4451

4552
class AuthenticationFailed(GitHubError):
46-
4753
"""Exception class for 401 responses.
4854
4955
Possible reasons:
5056
5157
- Need one time password (for two-factor authentication)
5258
- You are not authorized to access the resource
5359
"""
54-
5560
pass
5661

5762

5863
class ForbiddenError(GitHubError):
59-
6064
"""Exception class for 403 responses.
6165
6266
Possible reasons:
6367
6468
- Too many requests (you've exceeded the ratelimit)
6569
- Too many login failures
6670
"""
67-
6871
pass
6972

7073

7174
class NotFoundError(GitHubError):
72-
7375
"""Exception class for 404 responses."""
74-
7576
pass
7677

7778

7879
class MethodNotAllowed(GitHubError):
79-
8080
"""Exception class for 405 responses."""
81-
8281
pass
8382

8483

8584
class NotAcceptable(GitHubError):
86-
8785
"""Exception class for 406 responses."""
88-
8986
pass
9087

9188

9289
class UnprocessableEntity(GitHubError):
93-
9490
"""Exception class for 422 responses."""
95-
9691
pass
9792

9893

9994
class ClientError(GitHubError):
100-
10195
"""Catch-all for 400 responses that aren't specific errors."""
96+
pass
10297

10398

10499
class ServerError(GitHubError):
105-
106100
"""Exception class for 5xx responses."""
107-
108101
pass
109102

110103

github3/models.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
from datetime import datetime
1414
from logging import getLogger
1515

16+
from . import exceptions
1617
from .decorators import requires_auth
17-
from .exceptions import error_for
1818
from .null import NullObject
1919
from .session import GitHubSession
2020
from .utils import UTC
@@ -143,6 +143,10 @@ def _remove_none(data):
143143
def _instance_or_null(self, instance_class, json):
144144
if json is None:
145145
return NullObject(instance_class.__name__)
146+
if not isinstance(json, dict):
147+
return exceptions.UnprocessableResponseBody(
148+
"GitHub's API returned a body that could not be handled", json
149+
)
146150
try:
147151
return instance_class(json, self)
148152
except TypeError: # instance_class is not a subclass of GitHubCore
@@ -171,7 +175,7 @@ def _boolean(self, response, true_code, false_code):
171175
if status_code == true_code:
172176
return True
173177
if status_code != false_code and status_code >= 400:
174-
raise error_for(response)
178+
raise exceptions.error_for(response)
175179
return False
176180

177181
def _delete(self, url, **kwargs):

github3/structs.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
# -*- coding: utf-8 -*-
2-
from collections import Iterator
3-
from .models import GitHubCore
2+
import collections
3+
import functools
4+
45
from requests.compat import urlparse, urlencode
56

7+
from . import exceptions
8+
from . import models
9+
610

7-
class GitHubIterator(GitHubCore, Iterator):
11+
class GitHubIterator(models.GitHubCore, collections.Iterator):
812
"""The :class:`GitHubIterator` class powers all of the iter_* methods."""
913
def __init__(self, count, url, cls, session, params=None, etag=None,
1014
headers=None):
11-
GitHubCore.__init__(self, {}, session)
15+
models.GitHubCore.__init__(self, {}, session)
1216
#: Original number of items requested
1317
self.original = count
1418
#: Number of items left in the iterator
@@ -45,7 +49,7 @@ def _repr(self):
4549
return '<GitHubIterator [{0}, {1}]>'.format(self.count, self.path)
4650

4751
def __iter__(self):
48-
self.last_url, params, cls = self.url, self.params, self.cls
52+
self.last_url, params = self.url, self.params
4953
headers = self.headers
5054

5155
if 0 < self.count <= 100 and self.count != -1:
@@ -54,6 +58,10 @@ def __iter__(self):
5458
if 'per_page' not in params and self.count == -1:
5559
params['per_page'] = 100
5660

61+
cls = self.cls
62+
if issubclass(self.cls, models.GitHubCore):
63+
cls = functools.partial(self.cls, session=self)
64+
5765
while (self.count == -1 or self.count > 0) and self.last_url:
5866
response = self._get(self.last_url, params=params,
5967
headers=headers)
@@ -72,14 +80,19 @@ def __iter__(self):
7280

7381
# languages returns a single dict. We want the items.
7482
if isinstance(json, dict):
83+
if issubclass(self.cls, models.GitHubObject):
84+
raise exceptions.UnprocessableResponseBody(
85+
"GitHub's API returned a body that could not be"
86+
" handled", json
87+
)
7588
if json.get('ETag'):
7689
del json['ETag']
7790
if json.get('Last-Modified'):
7891
del json['Last-Modified']
7992
json = json.items()
8093

8194
for i in json:
82-
yield cls(i, self) if issubclass(cls, GitHubCore) else cls(i)
95+
yield cls(i)
8396
self.count -= 1 if self.count > 0 else 0
8497
if self.count == 0:
8598
break
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"recorded_with": "betamax/0.4.1", "http_interactions": [{"recorded_at": "2015-02-22T04:23:56", "request": {"headers": {"Content-Type": "application/json", "Accept-Charset": "utf-8", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", "User-Agent": "github3.py/1.0.0a1", "Accept": "application/vnd.github.v3.full+json"}, "body": {"encoding": "utf-8", "string": ""}, "method": "GET", "uri": "https://api.github.com/repos/sigmavirus24/github3.py"}, "response": {"headers": {"Access-Control-Allow-Origin": "*", "X-Content-Type-Options": "nosniff", "Access-Control-Allow-Credentials": "true", "Content-Type": "application/json; charset=utf-8", "Transfer-Encoding": "chunked", "Date": "Sun, 22 Feb 2015 04:23:56 GMT", "Access-Control-Expose-Headers": "ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval", "Content-Security-Policy": "default-src 'none'", "X-GitHub-Media-Type": "github.v3; param=full; format=json", "Status": "200 OK", "ETag": "W/\"07acb8446729c2cedb1aa44279995ef3\"", "Cache-Control": "public, max-age=60, s-maxage=60", "X-XSS-Protection": "1; mode=block", "X-Frame-Options": "deny", "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", "X-RateLimit-Reset": "1424580854", "Content-Encoding": "gzip", "Server": "GitHub.com", "X-RateLimit-Limit": "60", "X-GitHub-Request-Id": "451DE374:0FF4:16646F2B:54E959DC", "X-RateLimit-Remaining": "43", "Vary": "Accept, Accept-Encoding", "X-Served-By": "474556b853193c38f1b14328ce2d1b7d", "Last-Modified": "Sun, 22 Feb 2015 02:56:55 GMT"}, "body": {"base64_string": "H4sIAAAAAAAAA62YTZPiNhCG/wrlaxgEZgizrkrt7inJbQ+bSy6UbAtbNbbkkmQoxjX/Pa8sf5IKDKNcKDDqR69a3XK3moCnQbTdb9b7zWYZCFqyIAoybvI63q6qS7AMjnVRHLo/NM9KeuKq1uEzmY2SZ8FUEDVBITMuwJgOBcVOEz6vX7brZUBP1FB1qFWBcbkxlY4IcQ/1ylFrzVQihWHCrBJZkpo446+n37agZapjWGyAB1esinccZwyYJleCclMWVxLc1K3J1eCjLAp5BuVa9L2JyGBpPdlSuMg+SYFlQ6TJGXyHJb1bR3BtHhfVWjXYQG0OPLUcjQ1RLH1YWGcHWXb/3xuiWCVbYB3rRPHKcCkeFzizBk2qjAr+Rj9Hg7UGxEp7XEprBWt2Qiw+bu7MGlIpfqLJxbpGsYTxE5z9SeSVPYjmUtm0/QtBYV3PDTvQtLRpeKSFZu/LoJ3eYFD7YIms+2j0z9M8ZcOuYsIfF5NLsSh4rKi6LI5SLThyVh1pglhdnHGMLBCui9+5+aOOF99//Hmy2Ytxr4OSm5nbOn+WjHM5lnRnT24ikJ4AQNIru3hxrH1D8NnlU4JUp7FU1Mh7h8ZtgTNQQ6Y/bSwZRksv4S0AoFxKP0+2AIC41jX7UGjfXnjL0aTPH1GXsTvyPpI1t9GOAK1U45wXjHl5cIA0pD+VkQ4iyf2wPaMh7lu72zTzkmrtgYkLGXtx8KIkLaQhOqfuPWQOvuos1TJmUMWO3lItY4Aa5bnfrUwLGZB4CRpsvZfOnkGazqMFFVlNMz/qAMGu21d1Rt/uFjG3c2ekAGkrNMXj2v+QGzlWqasdkO9+Lh0xI7QtSG6XOXccMClsWheUJb9XF9wmdohZ2P8PWBun12j7+34Zc1+uZTRkPJPdod/Rfbzbnfq9zukcXTvgFRI9gzS/VNTk9uTCVBVVzEd0hyBNTFFsrVarJme0LatLpjwz2BGAoirJUTX66Gx6Bqqekpq2Wj9amSmq90LS1Mu3AwRAt40+Wh1huv8V+lAvgS1gSix5wbSRwu+MHSlTtpCGH3nykY7ldrrNQM1XzUXClrQolohawxOOOEatbXcRBSfz85AjYBm4BnCdSsEQ0l5eV8wxGuI6zUQxNCLpgRo0EOF6Ez6tt0+b7c/Nl2j3Eu22f2MldZXOxuye1uFTGP5ch9Hu12i3s2OqWucTzHTIPlqHdghOwC4E8Q1XDPjEtca/+vtJS2FvDWCodT4afhvNov+4/+jMkgKxdBX0H5/zdP1aum8KqbksWYUyobtJGVa5rS4reDpF+5XKRK/QAxO7Mv6GoS/7zX5WECSyFtiPcP+8DM7UoHbFq3f6sC8khqbPTk31waVpEBlV264ST8ZjYPLwzF/52HtilJWsezPXxXXTfQlxbHKlZHdBJJC1uAComOgmG3Thvsq1b5G1mYzAQvBfv45uWSk70rowB1dNYx0p2oBCVliIYOaMPrAHW9q0BOn9sHv/B529Yv8uEwAA", "encoding": "utf-8", "string": ""}, "status": {"message": "OK", "code": 200}, "url": "https://api.github.com/repos/sigmavirus24/github3.py"}}, {"recorded_at": "2015-02-22T04:23:56", "request": {"headers": {"Content-Type": "application/json", "Accept-Charset": "utf-8", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", "User-Agent": "github3.py/1.0.0a1", "Accept": "application/vnd.github.v3.full+json"}, "body": {"encoding": "utf-8", "string": ""}, "method": "GET", "uri": "https://api.github.com/repos/sigmavirus24/github3.py/git/refs/heads/develop?per_page=100"}, "response": {"headers": {"Access-Control-Allow-Origin": "*", "Content-Security-Policy": "default-src 'none'", "X-Content-Type-Options": "nosniff", "Access-Control-Allow-Credentials": "true", "Content-Type": "application/json; charset=utf-8", "Transfer-Encoding": "chunked", "Date": "Sun, 22 Feb 2015 04:23:56 GMT", "Access-Control-Expose-Headers": "ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval", "X-Frame-Options": "deny", "X-GitHub-Media-Type": "github.v3; param=full; format=json", "Status": "200 OK", "ETag": "W/\"f148066c5c6c3ba6ca6116fed0a94fb0\"", "Cache-Control": "public, max-age=60, s-maxage=60", "X-XSS-Protection": "1; mode=block", "X-Poll-Interval": "300", "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", "X-RateLimit-Reset": "1424580854", "Content-Encoding": "gzip", "Server": "GitHub.com", "X-RateLimit-Limit": "60", "X-GitHub-Request-Id": "451DE374:0FF4:16646F65:54E959DC", "X-RateLimit-Remaining": "42", "Vary": "Accept, Accept-Encoding", "X-Served-By": "d594a23ec74671eba905bf91ef329026", "Last-Modified": "Sun, 22 Feb 2015 02:56:55 GMT"}, "body": {"base64_string": "H4sIAAAAAAAAA6WOyw6DIBBF/4V146BgffwNyFRoNBAGTIzx34tx20WTbiaT3DtnzsEivth4TQKLyhAY3HDxgT1YjkuJbEqBRgAVXDW7ZLOuJr9CxOAJyM2r2lzM1Ei4U1GF/VpL4wvT6zdOiY0HI6sKHeVQ90J3WjWGSyOQy7ZW7TRgU3MueK+wH9pnV3TSHrBclOerS//r3RyCnw3O8wNTw7SJLgEAAA==", "encoding": "utf-8", "string": ""}, "status": {"message": "OK", "code": 200}, "url": "https://api.github.com/repos/sigmavirus24/github3.py/git/refs/heads/develop?per_page=100"}}]}

tests/integration/test_repos_repo.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"""Integration tests for Repositories."""
22
import github3
3+
import github3.exceptions as exc
34

4-
from .helper import IntegrationHelper
5+
import pytest
56

7+
from . import helper
68

7-
class TestRepository(IntegrationHelper):
9+
10+
class TestRepository(helper.IntegrationHelper):
811

912
"""Integration tests for the Repository object."""
1013

@@ -411,6 +414,15 @@ def test_refs(self):
411414
for ref in references:
412415
assert isinstance(ref, github3.git.Reference)
413416

417+
def test_refs_raises_unprocessable_exception(self):
418+
"""Verify github3.exceptions.UnprocessableResponseBody is raised."""
419+
cassette_name = self.cassette_name('invalid_refs')
420+
with self.recorder.use_cassette(cassette_name):
421+
repository = self.gh.repository('sigmavirus24', 'github3.py')
422+
assert repository is not None
423+
with pytest.raises(exc.UnprocessableResponseBody):
424+
list(repository.refs('heads/develop'))
425+
414426
def test_stargazers(self):
415427
"""Test the ability to retrieve the stargazers on a repository."""
416428
cassette_name = self.cassette_name('stargazers')

tests/unit/test_structs.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
class TestGitHubIterator(UnitHelper):
66
described_class = GitHubIterator
77

8-
def setUp(self):
9-
super(TestGitHubIterator, self).setUp()
10-
self.count = -1
11-
self.cls = object
8+
def after_setup(self):
9+
self.count = self.instance.count = -1
10+
self.cls = self.instance.cls = object
1211

1312
def create_instance_of_described_class(self):
1413
self.url = 'https://api.github.com/users'

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy