Skip to content

Commit e1d67ad

Browse files
authored
Fix requesting urls containing parameters with parameters dict (PyGithub#2929)
Requesting an url that contains parameters (query part of the url) did not support giving a parameters dict: Requester.requestJson(verb, "https://api.github.com/?per_page=10", {"per_page": 20}) Now, parameters given in the URL have precedence over the dict. Iterating over reversed `PaginatedList` is affected by this. Fixes PyGithub#1136.
1 parent 0d395d4 commit e1d67ad

File tree

6 files changed

+221
-11
lines changed

6 files changed

+221
-11
lines changed

github/PaginatedList.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,13 @@ def __reverse(self) -> None:
236236
lastUrl = self._getLastPageUrl()
237237
if lastUrl:
238238
self.__nextUrl = lastUrl
239+
if self.__nextParams:
240+
# #2929: remove all parameters from self.__nextParams contained in self.__nextUrl
241+
self.__nextParams = {
242+
k: v
243+
for k, v in self.__nextParams.items()
244+
if k not in Requester.get_parameters_of_url(self.__nextUrl).keys()
245+
}
239246

240247
def _couldGrow(self) -> bool:
241248
return self.__nextUrl is not None

github/Requester.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,29 @@ def get_graphql_prefix(path: Optional[str]) -> str:
471471
path = Requester.remove_suffix(path, "/v3")
472472
return path + "/graphql"
473473

474+
@staticmethod
475+
def get_parameters_of_url(url: str) -> Dict[str, list]:
476+
query = urllib.parse.urlparse(url)[4]
477+
return urllib.parse.parse_qs(query)
478+
479+
@staticmethod
480+
def add_parameters_to_url(
481+
url: str,
482+
parameters: Dict[str, Any],
483+
) -> str:
484+
scheme, netloc, url, params, query, fragment = urllib.parse.urlparse(url)
485+
url_params = urllib.parse.parse_qs(query)
486+
# union parameters in url with given parameters, the latter have precedence
487+
url_params.update(**{k: v if isinstance(v, list) else [v] for k, v in parameters.items()})
488+
parameter_list = [(key, value) for key, values in url_params.items() for value in values]
489+
# remove query from url
490+
url = urllib.parse.urlunparse((scheme, netloc, url, params, "", fragment))
491+
492+
if len(parameter_list) == 0:
493+
return url
494+
else:
495+
return f"{url}?{urllib.parse.urlencode(parameter_list)}"
496+
474497
def close(self) -> None:
475498
"""
476499
Close the connection to the server.
@@ -860,7 +883,7 @@ def __requestEncode(
860883
requestHeaders["User-Agent"] = self.__userAgent
861884

862885
url = self.__makeAbsoluteUrl(url)
863-
url = self.__addParametersToUrl(url, parameters)
886+
url = Requester.add_parameters_to_url(url, parameters)
864887

865888
encoded_input = None
866889
if input is not None:
@@ -996,16 +1019,6 @@ def __makeAbsoluteUrl(self, url: str) -> str:
9961019
url += f"?{o.query}"
9971020
return url
9981021

999-
def __addParametersToUrl(
1000-
self,
1001-
url: str,
1002-
parameters: Dict[str, Any],
1003-
) -> str:
1004-
if len(parameters) == 0:
1005-
return url
1006-
else:
1007-
return f"{url}?{urllib.parse.urlencode(parameters)}"
1008-
10091022
def __createConnection(
10101023
self,
10111024
) -> Union[HTTPRequestsConnectionClass, HTTPSRequestsConnectionClass]:

tests/PaginatedList.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
# #
3838
################################################################################
3939

40+
from datetime import datetime, timezone
41+
4042
from github.PaginatedList import PaginatedList as PaginatedListImpl
4143

4244
from . import Framework
@@ -326,6 +328,40 @@ def testCustomPerPageWithGetPage(self):
326328
self.g.per_page = 100
327329
self.assertEqual(len(self.repo.get_issues().get_page(2)), 100)
328330

331+
def testCustomPerPageIteration(self):
332+
self.g.per_page = 3
333+
repo = self.g.get_repo("PyGithub/PyGithub")
334+
comments = repo.get_issue(1136).get_comments()
335+
self.assertEqual(
336+
[
337+
datetime(2019, 8, 10, 18, 16, 46, tzinfo=timezone.utc),
338+
datetime(2024, 1, 6, 16, 4, 34, tzinfo=timezone.utc),
339+
datetime(2024, 1, 6, 17, 34, 11, tzinfo=timezone.utc),
340+
datetime(2024, 3, 20, 15, 24, 15, tzinfo=timezone.utc),
341+
datetime(2024, 3, 21, 10, 55, 14, tzinfo=timezone.utc),
342+
datetime(2024, 3, 21, 14, 2, 22, tzinfo=timezone.utc),
343+
datetime(2024, 3, 24, 13, 58, 57, tzinfo=timezone.utc),
344+
],
345+
[comment.created_at for comment in comments],
346+
)
347+
348+
def testCustomPerPageReversedIteration(self):
349+
self.g.per_page = 3
350+
repo = self.g.get_repo("PyGithub/PyGithub")
351+
comments = repo.get_issue(1136).get_comments().reversed
352+
self.assertEqual(
353+
[
354+
datetime(2024, 3, 24, 13, 58, 57, tzinfo=timezone.utc),
355+
datetime(2024, 3, 21, 14, 2, 22, tzinfo=timezone.utc),
356+
datetime(2024, 3, 21, 10, 55, 14, tzinfo=timezone.utc),
357+
datetime(2024, 3, 20, 15, 24, 15, tzinfo=timezone.utc),
358+
datetime(2024, 1, 6, 17, 34, 11, tzinfo=timezone.utc),
359+
datetime(2024, 1, 6, 16, 4, 34, tzinfo=timezone.utc),
360+
datetime(2019, 8, 10, 18, 16, 46, tzinfo=timezone.utc),
361+
],
362+
[comment.created_at for comment in comments],
363+
)
364+
329365
def testNoFirstPage(self):
330366
self.assertFalse(next(iter(self.list), None))
331367

tests/ReplayData/PaginatedList.testCustomPerPageIteration.txt

Lines changed: 54 additions & 0 deletions
Large diffs are not rendered by default.

tests/ReplayData/PaginatedList.testCustomPerPageReversedIteration.txt

Lines changed: 65 additions & 0 deletions
Large diffs are not rendered by default.

tests/Requester.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from unittest import mock
2929

3030
import github
31+
from github import Requester as gr
3132

3233
from . import Framework
3334
from .GithubIntegration import APP_ID, PRIVATE_KEY
@@ -137,6 +138,40 @@ class TestAuth(github.Auth.AppAuth):
137138
),
138139
)
139140

141+
def testGetParametersOfUrl(self):
142+
self.assertEqual({}, gr.Requester.get_parameters_of_url("https://github.com/api"))
143+
self.assertEqual({"per_page": ["10"]}, gr.Requester.get_parameters_of_url("https://github.com/api?per_page=10"))
144+
self.assertEqual(
145+
{"per_page": ["10"], "page": ["2"]},
146+
gr.Requester.get_parameters_of_url("https://github.com/api?per_page=10&page=2"),
147+
)
148+
self.assertEqual(
149+
{"item": ["1", "2", "3"]}, gr.Requester.get_parameters_of_url("https://github.com/api?item=1&item=2&item=3")
150+
)
151+
152+
def testAddParametersToUrl(self):
153+
self.assertEqual("https://github.com/api", gr.Requester.add_parameters_to_url("https://github.com/api", {}))
154+
self.assertEqual(
155+
"https://github.com/api?per_page=10",
156+
gr.Requester.add_parameters_to_url("https://github.com/api", {"per_page": 10}),
157+
)
158+
self.assertEqual(
159+
"https://github.com/api?per_page=10&page=2",
160+
gr.Requester.add_parameters_to_url("https://github.com/api", {"per_page": 10, "page": 2}),
161+
)
162+
self.assertEqual(
163+
"https://github.com/api?per_page=10&page=2",
164+
gr.Requester.add_parameters_to_url("https://github.com/api?per_page=10", {"page": 2}),
165+
)
166+
self.assertEqual(
167+
"https://github.com/api?per_page=10&page=2",
168+
gr.Requester.add_parameters_to_url("https://github.com/api?per_page=10&page=1", {"page": 2}),
169+
)
170+
self.assertEqual(
171+
"https://github.com/api?item=3&item=4",
172+
gr.Requester.add_parameters_to_url("https://github.com/api?item=1&item=2&item=3", {"item": [3, 4]}),
173+
)
174+
140175
def testCloseGithub(self):
141176
mocked_connection = mock.MagicMock()
142177
mocked_custom_connection = mock.MagicMock()

0 commit comments

Comments
 (0)