Skip to content

Commit 73236e2

Browse files
authored
Close connections after use (#2724)
1 parent fbf2e61 commit 73236e2

File tree

7 files changed

+160
-16
lines changed

7 files changed

+160
-16
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ g = Github(base_url="https://{hostname}/api/v3", auth=auth)
4343
# Then play with your Github objects:
4444
for repo in g.get_user().get_repos():
4545
print(repo.name)
46+
47+
# To close connections after use
48+
g.close()
4649
```
4750

4851
## Documentation

doc/introduction.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ Then play with your Github objects::
3535
# to see all the available attributes and methods
3636
print(dir(repo))
3737

38+
To close connections after use::
39+
40+
g.close()
41+
3842
Download and install
3943
--------------------
4044

github/GithubIntegration.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,23 @@ def __init__(
119119
seconds_between_writes=seconds_between_writes,
120120
)
121121

122+
def close(self) -> None:
123+
"""
124+
Close connections to the server. Alternatively, use the GithubIntegration object as a context manager:
125+
126+
.. code-block:: python
127+
128+
with github.GithubIntegration(...) as gi:
129+
# do something
130+
"""
131+
self.__requester.close()
132+
133+
def __enter__(self) -> "GithubIntegration":
134+
return self
135+
136+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
137+
self.close()
138+
122139
def get_github_for_installation(self, installation_id, token_permissions=None):
123140
# The installation has to authenticate as an installation, not an app
124141
auth = self.auth.get_installation_auth(installation_id, token_permissions, self.__requester)

github/MainClass.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,23 @@ def __init__(
184184
seconds_between_writes,
185185
)
186186

187+
def close(self) -> None:
188+
"""
189+
Close connections to the server. Alternatively, use the Github object as a context manager:
190+
191+
.. code-block:: python
192+
193+
with github.Github(...) as gh:
194+
# do something
195+
"""
196+
self.__requester.close()
197+
198+
def __enter__(self) -> "Github":
199+
return self
200+
201+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
202+
self.close()
203+
187204
@property
188205
def FIX_REPO_GET_GIT_REF(self):
189206
"""

github/Requester.py

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,28 @@
5757
import mimetypes
5858
import os
5959
import re
60+
import threading
6061
import time
6162
import urllib
6263
import urllib.parse
63-
from collections import defaultdict
64+
from collections import deque
6465
from datetime import datetime, timezone
6566
from io import IOBase
66-
from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, ItemsView, List, Optional, Tuple, Type, TypeVar, Union
67+
from typing import (
68+
TYPE_CHECKING,
69+
Any,
70+
Callable,
71+
Deque,
72+
Dict,
73+
Generic,
74+
ItemsView,
75+
List,
76+
Optional,
77+
Tuple,
78+
Type,
79+
TypeVar,
80+
Union,
81+
)
6782

6883
import requests
6984
import requests.adapters
@@ -166,7 +181,7 @@ def getresponse(self) -> RequestsResponse:
166181
return RequestsResponse(r)
167182

168183
def close(self) -> None:
169-
return
184+
self.session.close()
170185

171186

172187
class HTTPRequestsConnectionClass:
@@ -229,7 +244,7 @@ def getresponse(self) -> RequestsResponse:
229244
return RequestsResponse(r)
230245

231246
def close(self) -> None:
232-
return
247+
self.session.close()
233248

234249

235250
class Requester:
@@ -238,7 +253,6 @@ class Requester:
238253

239254
__httpConnectionClass = HTTPRequestsConnectionClass
240255
__httpsConnectionClass = HTTPSRequestsConnectionClass
241-
__connection = None
242256
__persist = True
243257
__logger: Optional[logging.Logger] = None
244258

@@ -337,7 +351,6 @@ def _initializeDebugFeature(self) -> None:
337351
__connectionClass: Union[Type[HTTPRequestsConnectionClass], Type[HTTPSRequestsConnectionClass]]
338352
__hostname: str
339353
__authorizationHeader: Optional[str]
340-
__last_requests: Dict[str, float]
341354
__seconds_between_requests: Optional[float]
342355
__seconds_between_writes: Optional[float]
343356

@@ -369,14 +382,17 @@ def __init__(
369382
self.__pool_size = pool_size
370383
self.__seconds_between_requests = seconds_between_requests
371384
self.__seconds_between_writes = seconds_between_writes
372-
self.__last_requests = defaultdict(lambda: 0.0)
385+
self.__last_requests: Dict[str, float] = dict()
373386
self.__scheme = o.scheme
374387
if o.scheme == "https":
375388
self.__connectionClass = self.__httpsConnectionClass
376389
elif o.scheme == "http":
377390
self.__connectionClass = self.__httpConnectionClass
378391
else:
379392
assert False, "Unknown URL scheme"
393+
self.__connection: Optional[Union[HTTPRequestsConnectionClass, HTTPSRequestsConnectionClass]] = None
394+
self.__connection_lock = threading.Lock()
395+
self.__custom_connections: Deque[Union[HTTPRequestsConnectionClass, HTTPSRequestsConnectionClass]] = deque()
380396
self.rate_limiting = (-1, -1)
381397
self.rate_limiting_resettime = 0
382398
self.FIX_REPO_GET_GIT_REF = True
@@ -397,6 +413,33 @@ def __init__(
397413
if isinstance(self.__auth, WithRequester):
398414
self.__auth.withRequester(self)
399415

416+
def __getstate__(self) -> Dict[str, Any]:
417+
state = self.__dict__.copy()
418+
# __connection_lock is not picklable
419+
del state["_Requester__connection_lock"]
420+
# __connection is not usable on remote, so ignore it
421+
del state["_Requester__connection"]
422+
# __custom_connections is not usable on remote, so ignore it
423+
del state["_Requester__custom_connections"]
424+
return state
425+
426+
def __setstate__(self, state: Dict[str, Any]) -> None:
427+
self.__dict__.update(state)
428+
self.__connection_lock = threading.Lock()
429+
self.__connection = None
430+
self.__custom_connections = deque()
431+
432+
def close(self) -> None:
433+
"""
434+
Close the connection to the server.
435+
"""
436+
with self.__connection_lock:
437+
if self.__connection is not None:
438+
self.__connection.close()
439+
self.__connection = None
440+
while self.__custom_connections:
441+
self.__custom_connections.popleft().close()
442+
400443
@property
401444
def kwargs(self) -> Dict[str, Any]:
402445
"""
@@ -499,13 +542,15 @@ def __customConnection(
499542
retry=self.__retry,
500543
pool_size=self.__pool_size,
501544
)
545+
self.__custom_connections.append(cnx)
502546
elif o.scheme == "https":
503547
cnx = self.__httpsConnectionClass(
504548
o.hostname, # type: ignore
505549
o.port,
506550
retry=self.__retry,
507551
pool_size=self.__pool_size,
508552
)
553+
self.__custom_connections.append(cnx)
509554
return cnx
510555

511556
@classmethod
@@ -717,7 +762,6 @@ def __requestRaw(
717762
responseHeaders = {k.lower(): v for k, v in response.getheaders()}
718763
output = response.read()
719764

720-
cnx.close()
721765
if input:
722766
if isinstance(input, IOBase):
723767
input.close()
@@ -822,14 +866,19 @@ def __createConnection(
822866
if self.__persist and self.__connection is not None:
823867
return self.__connection
824868

825-
self.__connection = self.__connectionClass(
826-
self.__hostname,
827-
self.__port,
828-
retry=self.__retry,
829-
pool_size=self.__pool_size,
830-
timeout=self.__timeout,
831-
verify=self.__verify,
832-
)
869+
with self.__connection_lock:
870+
if self.__connection is not None:
871+
if self.__persist:
872+
return self.__connection
873+
self.__connection.close()
874+
self.__connection = self.__connectionClass(
875+
self.__hostname,
876+
self.__port,
877+
retry=self.__retry,
878+
pool_size=self.__pool_size,
879+
timeout=self.__timeout,
880+
verify=self.__verify,
881+
)
833882

834883
return self.__connection
835884

tests/Pickle.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import pickle
2+
import unittest
3+
4+
import github
5+
from github.Repository import Repository
6+
7+
REPO_NAME = "PyGithub/PyGithub"
8+
9+
10+
class Pickle(unittest.TestCase):
11+
def testPickleGithub(self):
12+
gh = github.Github()
13+
gh2 = pickle.loads(pickle.dumps(gh))
14+
self.assertIsInstance(gh2, github.Github)
15+
self.assertIsNotNone(gh2._Github__requester._Requester__connection_lock)
16+
self.assertIsNone(gh2._Github__requester._Requester__connection)
17+
self.assertEqual(len(gh2._Github__requester._Requester__custom_connections), 0)
18+
19+
def testPickleRepository(self):
20+
gh = github.Github()
21+
repo = gh.get_repo(REPO_NAME, lazy=True)
22+
repo2 = pickle.loads(pickle.dumps(repo))
23+
self.assertIsInstance(repo2, Repository)
24+
self.assertIsNotNone(repo2._requester._Requester__connection_lock)
25+
self.assertIsNone(repo2._requester._Requester__connection)
26+
self.assertEqual(len(repo2._requester._Requester__custom_connections), 0)

tests/Requester.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import github
2727

2828
from . import Framework
29+
from .GithubIntegration import APP_ID, PRIVATE_KEY
2930

3031
REPO_NAME = "PyGithub/PyGithub"
3132

@@ -132,6 +133,33 @@ class TestAuth(github.Auth.AppAuth):
132133
),
133134
)
134135

136+
def testCloseGithub(self):
137+
mocked_connection = mock.MagicMock()
138+
mocked_custom_connection = mock.MagicMock()
139+
140+
with github.Github() as gh:
141+
requester = gh._Github__requester
142+
requester._Requester__connection = mocked_connection
143+
requester._Requester__custom_connections.append(mocked_custom_connection)
144+
145+
mocked_connection.close.assert_called_once_with()
146+
mocked_custom_connection.close.assert_called_once_with()
147+
self.assertIsNone(requester._Requester__connection)
148+
149+
def testCloseGithubIntegration(self):
150+
mocked_connection = mock.MagicMock()
151+
mocked_custom_connection = mock.MagicMock()
152+
153+
auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY)
154+
with github.GithubIntegration(auth=auth) as gi:
155+
requester = gi._GithubIntegration__requester
156+
requester._Requester__connection = mocked_connection
157+
requester._Requester__custom_connections.append(mocked_custom_connection)
158+
159+
mocked_connection.close.assert_called_once_with()
160+
mocked_custom_connection.close.assert_called_once_with()
161+
self.assertIsNone(requester._Requester__connection)
162+
135163
def testLoggingRedirection(self):
136164
self.assertEqual(self.g.get_repo("EnricoMi/test").name, "test-renamed")
137165
self.logger.info.assert_called_once_with(

0 commit comments

Comments
 (0)