Skip to content

Commit 035becd

Browse files
authored
Use Ghost user when user data is nil
Create and instantiate a default "Ghost" user based on the one GitHub uses to fill in when user data is not present in an API response and avoid a user object being None or causing other problems. Squashed from #1249
1 parent ad692fb commit 035becd

File tree

4 files changed

+72
-1
lines changed

4 files changed

+72
-1
lines changed

src/github3/users.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,11 @@ class _User(models.GitHubCore):
307307

308308
class_name = "_User"
309309

310+
def __init__(self, json, session):
311+
if json is None:
312+
json = _ghost_json
313+
super().__init__(json, session)
314+
310315
def _update_attributes(self, user):
311316
self.avatar_url = user["avatar_url"]
312317
self.events_urlt = URITemplate(user["events_url"])
@@ -869,7 +874,7 @@ class AuthenticatedUser(User):
869874
"""Object to represent the currently authenticated user.
870875
871876
This is returned by :meth:`~github3.github.GitHub.me`. It contains the
872-
extra informtation that is not returned for other users such as the
877+
extra information that is not returned for other users such as the
873878
currently authenticated user's plan and private email information.
874879
875880
.. versionadded:: 1.0.0
@@ -973,3 +978,42 @@ def _update_attributes(self, contributor):
973978
UserLike = t.Union[
974979
ShortUser, User, AuthenticatedUser, Collaborator, Contributor, str
975980
]
981+
982+
_ghost_json: t.Final[t.Dict[str, t.Any]] = {
983+
# from https://api.github.com/users/ghost
984+
"login": "ghost",
985+
"id": 10137,
986+
"node_id": "MDQ6VXNlcjEwMTM3",
987+
"avatar_url": "https://avatars.githubusercontent.com/u/10137?v=4",
988+
"gravatar_id": "",
989+
"url": "https://api.github.com/users/ghost",
990+
"html_url": "https://github.com/ghost",
991+
"followers_url": "https://api.github.com/users/ghost/followers",
992+
"following_url": "https://api.github.com/users/ghost/following{/other_user"
993+
"}",
994+
"gists_url": "https://api.github.com/users/ghost/gists{/gist_id}",
995+
"starred_url": "https://api.github.com/users/ghost/starred{/owner}{/repo}",
996+
"subscriptions_url": "https://api.github.com/users/ghost/subscriptions",
997+
"organizations_url": "https://api.github.com/users/ghost/orgs",
998+
"repos_url": "https://api.github.com/users/ghost/repos",
999+
"events_url": "https://api.github.com/users/ghost/events{/privacy}",
1000+
"received_events_url": "https://api.github.com/users/ghost/received_events",
1001+
"type": "User",
1002+
"user_view_type": "public",
1003+
"site_admin": False,
1004+
"name": "Deleted user",
1005+
"company": None,
1006+
"blog": "",
1007+
"location": "Nothing to see here, move along.",
1008+
"email": None,
1009+
"hireable": None,
1010+
"bio": "Hi, I'm @ghost! I take the place of user accounts that have been "
1011+
"deleted.\n:ghost:\n",
1012+
"twitter_username": None,
1013+
"public_repos": 0,
1014+
"public_gists": 0,
1015+
"followers": 11584,
1016+
"following": 0,
1017+
"created_at": "2008-05-13T06:14:25Z",
1018+
"updated_at": "2018-04-10T17:22:33Z",
1019+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"http_interactions": [{"request": {"body": {"encoding": "utf-8", "string": ""}, "headers": {"User-Agent": ["github3.py/4.0.1"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["application/vnd.github.v3.full+json"], "Connection": ["keep-alive"], "Accept-Charset": ["utf-8"], "Content-Type": ["application/json"]}, "method": "GET", "uri": "https://api.github.com/repos/qiskit-community/qiskit-qec"}, "response": {"body": {"encoding": "utf-8", "base64_string": "H4sIAAAAAAAAA+2ZbW/bNhDHv0qgt4sjW42bxkDRDWhXbFibdUixIsMgUBJtMZZEhaTs2kK++/4nSpbkArEb+c2AAkEeaN6Pp+Pd6e5SOiJyZi+uJpPx1fX15bmTyYj7tOZ8ePtufZP8noTvr7fsy1+rMFtuP27fff3wNpze3H567WAzSzl2Pgi9FGb0wEOszYsk8fsfhDJNi0yYjdvbmSuxYgaAOUs0P3fkOuPKmZVOIhcia7k7cdBJs6nnTSeXV/vKbm6W15s779eCfcnj6H2yCu4/bz7cf/768W04hijDYUz5hUqAjo3J9cx17aK+WAgTF0GhuQplZnhmLnCqW7jNWW9Wry/BWKiaUlkIC3u0XNQkKw6cbp65+xSxSZM9RawClVhtpK7AXCaJXIO2r/4xB7o7abqeiiSyxQASpEtXmpjDmnjERzKM0OZ5ylWSpUs/4HfE0rgmxaNnKVjLQj1ypsfSVTyXFbQIdKhEboTMnqdojwCiVAuWiS17PhEEDRCp+DyVKkkQ+Aoe+zyEFS3dKhjDDZlK8ZCLFS5gAHaPAarZ5JQrbjpWwyr5j78SfO3Xn+dFkAjKI1oY7rMopUxQJYjHc0Tt98RNP9tEfHf/UONTlbLOHgqWmSI940pJdRZK+F1I93k2V0hta6mW0GROP5ok9WTAV/fxTfz29SDagas6iEFEAwLVlnwzmEWM0sX3OvxCZAgWSMWMPJRvDivag5Vu909yNcNZOvgBKghgsZTDLVtBABNaF/yoCDhshIql3SbUsiINbNY8JsAO4y0FOjOtxSLjfLBFd6DSbRJ8oFgWxsPRDad07W+VF7DFYJWJAVSQyGAwC+9itwKVro6Zfb0Z/xRaEpk4PbDi85OoTJwd2KgT+EGlLoF2WLxfDVxisL4Nxy1rCycsWxRsMZy8A8EbqBpYsO3BuulwjLUkYKlEVCIoTpMgWxZpbMsU5IfhJm5RLbiqf56urI4wRqeWqsyRpuJQ+XGYWmN6oXEiNPnxPp7+Plw1Hac2cUq3zev25VGfMNTa9duj0bd7Tt2rDHaVhuOWP+XMxJTxcFzOFB+qfI1xy4Chgru4uChjzqoqP+XqBNFuKcAxFcYoWofqWzYcVFgpM1UDMSd1IzQUiWTRYFvvQIDaqx2qs6V0/SJHDz5Y0QrSpaYi4drIbHiObkldfiaNmIvwmIbqcFj2YOUbLbKQn7MkOYdXGxEK+DnaWLpZFLt8uLUsBY+DvsE2UQmHyw++BcUtp3RtcxzxPJGbk2SuDooCXnEMYyKfGbRH3tibjMbTkffydvJyNn0x817dYU+RR70909H41cjzbidXs+kEX7QnL3TcwWDLZORd3U7GM+8FBk20Bem49nn8hiHME7OPTu9EkxUIax23wj+3orP9scm3omEC592Ltu87e7X/7jxOHGrHMuU56pvO7Glf4WZ0JGRHdZceWWwh542vvTGGXp2iJpRFhuua0PKaGdTnKBu6i00xhFP/3JhYZqQJ075NGc7MqAKDN1rJlbxH76u7a22q6mxci6XoCVLR1pMSOizQiWDO0izbnrdW7HKC5C+o4bZXkSHP7PI3xmr1NDASmgUJbxdkzrNa8eYZvSuErwh5pmGfkrphPCjLGQwx8i5o6FcPIn+p1s7+sHvP7Gc6j77aUaf9uBZ5ssOvT9Nu/5TO0DR8efs+ub/7e7q9u/3ttYOpBbKOXPtkA2ScxiRC+4anedIdgK55gCejYsqnRk7O577iD4XAMGxnFiNzEcKw/zjdEUU9xxhVc4xRO8cg5ynUnIUcixF3/j13VkKLQCSYxsJWu2mLHSjM6G46dobTwcKNZ9WOFvE5KxLj2/4NkJRhRkO9fJr7NsSMXHKMbezNwhmMTMnBco7US85Swird8dmPoe83QyMY9MfQF1F2xEDd/TH0bf+fcZTB/ldD34wbGsQ2OZ9SVLdLrt8q14//Afb+uGbLGgAA", "string": ""}, "headers": {"Date": ["Tue, 09 Sep 2025 21:35:05 GMT"], "Content-Type": ["application/json; charset=utf-8"], "Cache-Control": ["public, max-age=60, s-maxage=60"], "Vary": ["Accept,Accept-Encoding, Accept, X-Requested-With"], "ETag": ["W/\"11595d7f6763d9af4e2c76d5b1d24e559ac64c96a3efd52b22d5e1cef13dab0f\""], "Last-Modified": ["Fri, 22 Aug 2025 17:51:51 GMT"], "X-GitHub-Media-Type": ["github.v3; param=full; format=json"], "x-github-api-version-selected": ["2022-11-28"], "Access-Control-Expose-Headers": ["ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset"], "Access-Control-Allow-Origin": ["*"], "Strict-Transport-Security": ["max-age=31536000; includeSubdomains; preload"], "X-Frame-Options": ["deny"], "X-Content-Type-Options": ["nosniff"], "X-XSS-Protection": ["0"], "Referrer-Policy": ["origin-when-cross-origin, strict-origin-when-cross-origin"], "Content-Security-Policy": ["default-src 'none'"], "Content-Encoding": ["gzip"], "Server": ["github.com"], "Accept-Ranges": ["bytes"], "X-RateLimit-Limit": ["60"], "X-RateLimit-Remaining": ["58"], "X-RateLimit-Reset": ["1757455665"], "X-RateLimit-Resource": ["core"], "X-RateLimit-Used": ["2"], "Content-Length": ["1473"], "X-GitHub-Request-Id": ["2063:3236F0:947458:84448C:68C09D89"]}, "status": {"code": 200, "message": "OK"}, "url": "https://api.github.com/repos/qiskit-community/qiskit-qec"}, "recorded_at": "2025-09-09T21:35:05"}, {"request": {"body": {"encoding": "utf-8", "string": ""}, "headers": {"User-Agent": ["github3.py/4.0.1"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["application/vnd.github.v3.full+json"], "Connection": ["keep-alive"], "Accept-Charset": ["utf-8"], "Content-Type": ["application/json"]}, "method": "GET", "uri": "https://api.github.com/repos/qiskit-community/qiskit-qec/releases/63525446"}, "response": {"body": {"encoding": "utf-8", "base64_string": "H4sIAAAAAAAAA61TwW7bMAz9FUHnOEpTJ8CEILt067DLgGLYYcPg0pYaC5UtVaK7ZUX/fZSrFIWxoVlX2Cfq8fHhPfKOD8FyyVtEH6UQ4M18Z7Ad6nnjOhG0d1HcmHhtsKBCN/QG94fCjW4IYTVEHcX6dLVcleWazzjEqDFWr80sHnhpwOCtAzUZ8FCM/yk/D7l720OnZxZqbe9pYoudncx7YtNRBiHsxO1iTl8B1rdArEZxefCNbBuwdYHLfrB2xnundJUA/OJddf3j7NP7du26WJ7tv3w8p16iq5JGAkxYEcJOY5XiMmhimxA6RON60++oM3dl0RRikZ/pTQW4Qi6vwEZN+ihwhNrSkFzxQefEucQwEKYJGlCrCqiNLxfLZbEo6f98cirLN3J18nXMS/0Ns1rIcp0wfqgtif0z0yMqr4D89j05EGqw01j+eYMzzTSbX8a/BnummbLXTu2rtFRk2sZvL8YronjY0yxnzAfTQTB2z6Jjm4ZWYnu41Lx+pu7GSz0P0OjiAwSvg6AiXavSNV1mkW54I8Ze1kDPyGVk2JrIILJbsEaxnP9G+C0lMWpD/TMFepSwF0l6RkvWcayEyxdpuHxOxP1vqebu0yAFAAA=", "string": ""}, "headers": {"Date": ["Tue, 09 Sep 2025 21:35:05 GMT"], "Content-Type": ["application/json; charset=utf-8"], "Cache-Control": ["public, max-age=60, s-maxage=60"], "Vary": ["Accept,Accept-Encoding, Accept, X-Requested-With"], "ETag": ["W/\"5e5605ebcad750bc7a39665523e25a157ef102e158e52019afe3f658db409de5\""], "Last-Modified": ["Mon, 04 Apr 2022 13:50:46 GMT"], "X-GitHub-Media-Type": ["github.v3; param=full; format=json"], "x-github-api-version-selected": ["2022-11-28"], "Access-Control-Expose-Headers": ["ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset"], "Access-Control-Allow-Origin": ["*"], "Strict-Transport-Security": ["max-age=31536000; includeSubdomains; preload"], "X-Frame-Options": ["deny"], "X-Content-Type-Options": ["nosniff"], "X-XSS-Protection": ["0"], "Referrer-Policy": ["origin-when-cross-origin, strict-origin-when-cross-origin"], "Content-Security-Policy": ["default-src 'none'"], "Content-Encoding": ["gzip"], "Server": ["github.com"], "Accept-Ranges": ["bytes"], "X-RateLimit-Limit": ["60"], "X-RateLimit-Remaining": ["57"], "X-RateLimit-Reset": ["1757455665"], "X-RateLimit-Resource": ["core"], "X-RateLimit-Used": ["3"], "Content-Length": ["467"], "X-GitHub-Request-Id": ["2063:3236F0:94752F:84455F:68C09D89"]}, "status": {"code": 200, "message": "OK"}, "url": "https://api.github.com/repos/qiskit-community/qiskit-qec/releases/63525446"}, "recorded_at": "2025-09-09T21:35:05"}], "recorded_with": "betamax/0.9.0"}

tests/integration/test_github.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,17 @@ def test_user(self):
774774
" encoded"
775775
)
776776

777+
def test_release_by_ghostuser(self):
778+
"""Test to retrieve a release with "author: null" (ghost user)."""
779+
cassette_name = self.cassette_name("release_author_null")
780+
with self.recorder.use_cassette(cassette_name):
781+
repository = self.gh.repository("qiskit-community", "qiskit-qec")
782+
release = repository.release(63525446)
783+
784+
assert isinstance(release, github3.repos.release.Release)
785+
assert isinstance(release.author, github3.users.ShortUser)
786+
assert release.author.login == "ghost"
787+
777788
def test_unfollow(self):
778789
"""Test the ability to unfollow a user."""
779790
self.token_login()

tests/unit/test_users.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,21 @@ def test_is_following(self):
6767
)
6868

6969

70+
class TestGhostUser(helper.UnitHelper):
71+
"""Test methods on Ghost User class."""
72+
73+
described_class = github3.users.User
74+
example_data = get_users_example_data()
75+
76+
def after_setup(self):
77+
self.instance = github3.users.User(None, self.session)
78+
79+
def test_str(self):
80+
"""Show that instance string and repr is ghost."""
81+
assert str(self.instance) == "ghost"
82+
assert repr(self.instance) == "<User [ghost:Deleted user]>"
83+
84+
7085
class TestUserGPGKeyRequiresAuth(helper.UnitRequiresAuthenticationHelper):
7186
"""Unit tests that demonstrate which GPGKey methods require auth."""
7287

0 commit comments

Comments
 (0)