Repository: PyGithub/PyGithub

Issue:
PaginatedList.totalCount is broken when there are 0 results
Self-contained test:
```
from github import Github

github = Github()
repos = github.search_repositories('shouldreturn0repos')

assert repos.totalCount == 0
```

The `totalCount` method has this code:
```
    def totalCount(self):
        if not self.__totalCount:
            [...]
            if 'link' not in headers:
                self.__totalCount = len(data) if data else 0
            [...]
```
The response has no `link` header but it has this data:
```
{"total_count":0,"incomplete_results":false,"items":[]}
```
and `totalCount` returns 3 because there are 3 items inside the data dict.

I'm not sure why the `total_count` value in the response is not used directly.


Discussion:
Good catch. Probably a bug in the code.

> I'm not sure why the total_count value in the response is not used directly.

It's because for some response (I think for all non-search related API), the response json did not contain the `total_count` key, so we have to get it from the `link` header.
I think @sfdye is right. We should check the returned JSON for a total_count item first, and then fall back if the key doesn't exist. I'll dig into this later today.

Resolving patch:
diff --git a/github/PaginatedList.py b/github/PaginatedList.py
--- a/github/PaginatedList.py
+++ b/github/PaginatedList.py
@@ -155,7 +155,12 @@ def totalCount(self):
                 headers=self.__headers
             )
             if 'link' not in headers:
-                self.__totalCount = len(data) if data else 0
+                if data and "total_count" in data:
+                    self.__totalCount = data["total_count"]
+                elif data:
+                    self.__totalCount = len(data)
+                else:
+                    self.__totalCount = 0
             else:
                 links = self.__parseLinkHeader(headers)
                 lastUrl = links.get("last")


Test patch:
diff --git a/github/tests/ReplayData/Search.testSearchReposWithNoResults.txt b/github/tests/ReplayData/Search.testSearchReposWithNoResults.txt
new file mode 100644
--- /dev/null
+++ b/github/tests/ReplayData/Search.testSearchReposWithNoResults.txt
@@ -0,0 +1,10 @@
+https
+GET
+api.github.com
+None
+/search/repositories?q=doesnotexist&per_page=1
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+None
+200
+[('status', '200 OK'), ('x-ratelimit-remaining', '29'), ('x-github-media-type', 'github.beta; format=json'), ('x-content-type-options', 'nosniff'), ('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-github-request-id', '78F11E32:5372:9B9CAA5:531405FC'), ('access-control-allow-credentials', 'true'), ('vary', 'Accept-Encoding'), ('content-length', '140961'), ('server', 'GitHub.com'), ('x-ratelimit-limit', '30'), ('cache-control', 'no-cache'), ('date', 'Thu, 22 Nov 2018 14:17:01 GMT'), ('access-control-allow-origin', '*'), ('content-type', 'application/json; charset=utf-8'), ('x-ratelimit-reset', '1393821241')]
+{"total_count":0,"incomplete_results":false,"items":[]}
diff --git a/github/tests/Search.py b/github/tests/Search.py
--- a/github/tests/Search.py
+++ b/github/tests/Search.py
@@ -53,6 +53,10 @@ def testSearchRepos(self):
         repos = self.g.search_repositories("github", sort="stars", order="desc", language="Python")
         self.assertListKeyBegin(repos, lambda r: r.full_name, [u'kennethreitz/legit', u'RuudBurger/CouchPotatoV1', u'gelstudios/gitfiti', u'gpjt/webgl-lessons', u'jacquev6/PyGithub', u'aaasen/github_globe', u'hmason/gitmarks', u'dnerdy/factory_boy', u'binaryage/drydrop', u'bgreenlee/sublime-github', u'karan/HackerNewsAPI', u'mfenniak/pyPdf', u'skazhy/github-decorator', u'llvmpy/llvmpy', u'lexrupy/gmate', u'ask/python-github2', u'audreyr/cookiecutter-pypackage', u'tabo/django-treebeard', u'dbr/tvdb_api', u'jchris/couchapp', u'joeyespo/grip', u'nigelsmall/py2neo', u'ask/chishop', u'sigmavirus24/github3.py', u'jsmits/github-cli', u'lincolnloop/django-layout', u'amccloud/django-project-skel', u'Stiivi/brewery', u'webpy/webpy.github.com', u'dustin/py-github', u'logsol/Github-Auto-Deploy', u'cloudkick/libcloud', u'berkerpeksag/github-badge', u'bitprophet/ssh', u'azavea/OpenTreeMap'])
 
+    def testSearchReposWithNoResults(self):
+        repos = self.g.search_repositories("doesnotexist")
+        self.assertEqual(repos.totalCount, 0)
+
     def testSearchIssues(self):
         issues = self.g.search_issues("compile", sort="comments", order="desc", language="C++")
         self.assertListKeyBegin(issues, lambda i: i.id, [12068673, 23250111, 14371957, 9423897, 24277400, 2408877, 11338741, 13980502, 27697165, 23102422])
@@ -67,7 +71,7 @@ def testSearchTopics(self):
 
     def testPaginateSearchTopics(self):
         repos = self.g.search_topics("python", repositories=">950")
-        self.assertEqual(repos.totalCount, 3)
+        self.assertEqual(repos.totalCount, 6)
 
     def testSearchCode(self):
         files = self.g.search_code("toto", sort="indexed", order="asc", user="jacquev6")