Skip to content

Commit

Permalink
Raise exception if json is not the expected type
Browse files Browse the repository at this point in the history
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 #310
  • Loading branch information
sigmavirus24 committed Feb 22, 2015
1 parent 28f2db8 commit fee85cf
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 32 deletions.
29 changes: 11 additions & 18 deletions github3/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@


class GitHubError(Exception):

"""The base exception class."""

def __init__(self, resp):
Expand Down Expand Up @@ -35,76 +34,70 @@ def message(self):
return self.msg


class BadRequest(GitHubError):
class UnprocessableResponseBody(GitHubError):
"""Exception class for response objects that cannot be handled."""
def __init__(self, message, body):
Exception.__init__(self, message)
self.body = body

def __str__(self):
return self.message

"""Exception class for 400 responses."""

class BadRequest(GitHubError):
"""Exception class for 400 responses."""
pass


class AuthenticationFailed(GitHubError):

"""Exception class for 401 responses.
Possible reasons:
- Need one time password (for two-factor authentication)
- You are not authorized to access the resource
"""

pass


class ForbiddenError(GitHubError):

"""Exception class for 403 responses.
Possible reasons:
- Too many requests (you've exceeded the ratelimit)
- Too many login failures
"""

pass


class NotFoundError(GitHubError):

"""Exception class for 404 responses."""

pass


class MethodNotAllowed(GitHubError):

"""Exception class for 405 responses."""

pass


class NotAcceptable(GitHubError):

"""Exception class for 406 responses."""

pass


class UnprocessableEntity(GitHubError):

"""Exception class for 422 responses."""

pass


class ClientError(GitHubError):

"""Catch-all for 400 responses that aren't specific errors."""
pass


class ServerError(GitHubError):

"""Exception class for 5xx responses."""

pass


Expand Down
8 changes: 6 additions & 2 deletions github3/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
from datetime import datetime
from logging import getLogger

from . import exceptions
from .decorators import requires_auth
from .exceptions import error_for
from .null import NullObject
from .session import GitHubSession
from .utils import UTC
Expand Down Expand Up @@ -143,6 +143,10 @@ def _remove_none(data):
def _instance_or_null(self, instance_class, json):
if json is None:
return NullObject(instance_class.__name__)
if not isinstance(json, dict):
return exceptions.UnprocessableResponseBody(
"GitHub's API returned a body that could not be handled", json
)
try:
return instance_class(json, self)
except TypeError: # instance_class is not a subclass of GitHubCore
Expand Down Expand Up @@ -171,7 +175,7 @@ def _boolean(self, response, true_code, false_code):
if status_code == true_code:
return True
if status_code != false_code and status_code >= 400:
raise error_for(response)
raise exceptions.error_for(response)
return False

def _delete(self, url, **kwargs):
Expand Down
25 changes: 19 additions & 6 deletions github3/structs.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
# -*- coding: utf-8 -*-
from collections import Iterator
from .models import GitHubCore
import collections
import functools

from requests.compat import urlparse, urlencode

from . import exceptions
from . import models


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

def __iter__(self):
self.last_url, params, cls = self.url, self.params, self.cls
self.last_url, params = self.url, self.params
headers = self.headers

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

cls = self.cls
if issubclass(self.cls, models.GitHubCore):
cls = functools.partial(self.cls, session=self)

while (self.count == -1 or self.count > 0) and self.last_url:
response = self._get(self.last_url, params=params,
headers=headers)
Expand All @@ -72,14 +80,19 @@ def __iter__(self):

# languages returns a single dict. We want the items.
if isinstance(json, dict):
if issubclass(self.cls, models.GitHubObject):
raise exceptions.UnprocessableResponseBody(
"GitHub's API returned a body that could not be"
" handled", json
)
if json.get('ETag'):
del json['ETag']
if json.get('Last-Modified'):
del json['Last-Modified']
json = json.items()

for i in json:
yield cls(i, self) if issubclass(cls, GitHubCore) else cls(i)
yield cls(i)
self.count -= 1 if self.count > 0 else 0
if self.count == 0:
break
Expand Down
1 change: 1 addition & 0 deletions tests/cassettes/Repository_invalid_refs.json
Original file line number Diff line number Diff line change
@@ -0,0 +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"}}]}
16 changes: 14 additions & 2 deletions tests/integration/test_repos_repo.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""Integration tests for Repositories."""
import github3
import github3.exceptions as exc

from .helper import IntegrationHelper
import pytest

from . import helper

class TestRepository(IntegrationHelper):

class TestRepository(helper.IntegrationHelper):

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

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

def test_refs_raises_unprocessable_exception(self):
"""Verify github3.exceptions.UnprocessableResponseBody is raised."""
cassette_name = self.cassette_name('invalid_refs')
with self.recorder.use_cassette(cassette_name):
repository = self.gh.repository('sigmavirus24', 'github3.py')
assert repository is not None
with pytest.raises(exc.UnprocessableResponseBody):
list(repository.refs('heads/develop'))

def test_stargazers(self):
"""Test the ability to retrieve the stargazers on a repository."""
cassette_name = self.cassette_name('stargazers')
Expand Down
7 changes: 3 additions & 4 deletions tests/unit/test_structs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
class TestGitHubIterator(UnitHelper):
described_class = GitHubIterator

def setUp(self):
super(TestGitHubIterator, self).setUp()
self.count = -1
self.cls = object
def after_setup(self):
self.count = self.instance.count = -1
self.cls = self.instance.cls = object

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

0 comments on commit fee85cf

Please sign in to comment.