Skip to content

Commit 4d7aa3d

Browse files
fix: properly pass kwargs to gitlab client and graphql client (gitlabform#1151)
In python-gitlab client, arbitrary parameters to graphql client is not accepted gracefully. This change fixes this issue by inspecting the library's signature and passing valid parameters to it. This way, we still maintain ability to pass accepted parameters/configurations through gitlabform's config. Related to gitlabform#1140
1 parent 734a39f commit 4d7aa3d

File tree

3 files changed

+158
-45
lines changed

3 files changed

+158
-45
lines changed

docs/reference/index.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,20 @@ gitlab:
4141
ssl_verify: true
4242
# timeout for the whole requests to the GitLab API, in seconds
4343
timeout: 10
44+
4445
# ** advanced parameters **
45-
# Any additional parameters specified here will be passed directly to the python-gitlab library.
46+
# Any additional parameters specified here will be passed to the python-gitlab library if they are supported.
4647
# See https://python-gitlab.readthedocs.io/en/stable/api-usage.html#gitlab-client for available options.
4748
#
48-
# GitlabForm hasn't yet 100% migrated to use python-gitlab. Some processors are not using it and wont use these parameters.
49+
# GitlabForm hasn't yet 100% migrated to use python-gitlab. Some processors are not using it and won't use these parameters.
4950
# For following migration to python-gitlab, see: https://github.com/gitlabform/gitlabform/issues/73
5051
#
51-
# For example:
52-
# retry_transient_errors: false # (defaults to `true`) retry requests that fail due to transient errors
53-
# obey_rate_limit: true # automatically wait if rate limit is hit
54-
# max_retries: 5 # number of times to retry failed requests
52+
# REST API parameters (passed to python-gitlab's Gitlab client):
53+
# retry_transient_errors: false # (defaults to `true`) retry requests that fail due to transient errors (5xx)
54+
#
55+
# GraphQL API parameters (passed to python-gitlab's GraphQL client):
56+
# max_retries: 10 # number of times to retry failed GraphQL requests
57+
# obey_rate_limit: true # automatically wait and retry if rate limit is hit
5558

5659
# Configuration to apply to GitLab projects, groups and subgroups
5760
projects_and_groups:

gitlabform/gitlab/__init__.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import enum
2+
import inspect
23

34
from typing import List
45

5-
from gitlab import GraphQL
6+
from gitlab import Gitlab as GitlabClient, GraphQL
67

78
from gitlabform.gitlab.commits import GitLabCommits
89
from gitlabform.gitlab.group_badges import GitLabGroupBadges
@@ -67,28 +68,41 @@ class GitLab(
6768

6869

6970
class GitlabWrapper:
71+
# Parameters accepted by python-gitlab's Gitlab.__init__
72+
# Other config keys (like max_retries) are used elsewhere in gitlabform
73+
# or passed to specific components like GraphQL
74+
GITLAB_CLIENT_PARAMS = set(inspect.signature(GitlabClient.__init__).parameters.keys()) - {"self"}
75+
76+
# Parameters accepted by python-gitlab's GraphQL.__init__
77+
GRAPHQL_PARAMS = set(inspect.signature(GraphQL.__init__).parameters.keys()) - {"self"}
78+
7079
def __init__(self, gitlabform: GitLab):
7180
session = gitlabform.session
7281

73-
graphql = GraphQL(url=gitlabform.gitlab_config["url"], token=gitlabform.gitlab_config["token"])
82+
graphql_kwargs = {k: v for k, v in gitlabform.gitlab_config.items() if k in self.GRAPHQL_PARAMS}
83+
graphql = GraphQL(**graphql_kwargs)
7484

75-
default_kwargs = {
85+
default_gitlab_kwargs = {
7686
"retry_transient_errors": True,
7787
}
78-
renamed_kwargs = {
88+
renamed_gitlab_kwargs = {
7989
"token": "private_token",
8090
}
81-
extra_kwargs = {
82-
**default_kwargs,
83-
**{k: v for k, v in gitlabform.gitlab_config.items() if k not in renamed_kwargs},
84-
**{renamed_kwargs[k]: v for k, v in gitlabform.gitlab_config.items() if k in renamed_kwargs},
91+
extra_gitlab_kwargs = {
92+
**default_gitlab_kwargs,
93+
**{
94+
k: v
95+
for k, v in gitlabform.gitlab_config.items()
96+
if k not in renamed_gitlab_kwargs and k in self.GITLAB_CLIENT_PARAMS
97+
},
98+
**{renamed_gitlab_kwargs[k]: v for k, v in gitlabform.gitlab_config.items() if k in renamed_gitlab_kwargs},
8599
}
86100

87101
self._gitlab: PythonGitlab = PythonGitlab(
88102
api_version="4",
89103
graphql=graphql,
90104
session=session,
91-
**extra_kwargs,
105+
**extra_gitlab_kwargs,
92106
)
93107

94108
def get_gitlab(self):

tests/unit/gitlab/test_python_gitlab.py

Lines changed: 126 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -68,50 +68,124 @@ def test_convert_result_to_member_roles_removes_GitlabGraphQl_specific_prefix_fr
6868

6969
class TestGitlabWrapperExtraKwargs:
7070
"""
71-
Tests for extra kwargs support in GitlabWrapper that should be passed to PythonGitlab.
72-
This includes retry_transient_errors, obey_rate_limit, and any other additional parameters
73-
defined in the configuration.
71+
Tests for GitlabWrapper kwargs filtering.
72+
Ensures that only valid parameters are passed to python-gitlab's Gitlab client
73+
and GraphQL client, preventing TypeError from unsupported kwargs.
7474
"""
7575

76+
@patch("gitlabform.gitlab.GraphQL")
77+
@patch("gitlabform.gitlab.PythonGitlab")
78+
def test_max_retries_not_passed_to_gitlab_client(self, mock_python_gitlab, mock_graphql):
79+
"""Test that max_retries is NOT passed to PythonGitlab (would cause TypeError)"""
80+
mock_gitlab = MagicMock(spec=GitLab)
81+
mock_gitlab.session = MagicMock()
82+
mock_gitlab.gitlab_config = {
83+
"url": "https://gitlab.example.com",
84+
"token": "test-token",
85+
"max_retries": 5, # This should NOT be passed to PythonGitlab
86+
}
87+
88+
_ = GitlabWrapper(mock_gitlab)
89+
90+
call_kwargs = mock_python_gitlab.call_args.kwargs
91+
assert "max_retries" not in call_kwargs
92+
93+
@patch("gitlabform.gitlab.GraphQL")
94+
@patch("gitlabform.gitlab.PythonGitlab")
95+
def test_max_retries_passed_to_graphql_client(self, mock_python_gitlab, mock_graphql):
96+
"""Test that max_retries IS passed to GraphQL client"""
97+
mock_gitlab = MagicMock(spec=GitLab)
98+
mock_gitlab.session = MagicMock()
99+
mock_gitlab.gitlab_config = {
100+
"url": "https://gitlab.example.com",
101+
"token": "test-token",
102+
"max_retries": 5,
103+
}
104+
105+
_ = GitlabWrapper(mock_gitlab)
106+
107+
call_kwargs = mock_graphql.call_args.kwargs
108+
assert call_kwargs["max_retries"] == 5
109+
110+
@patch("gitlabform.gitlab.GraphQL")
76111
@patch("gitlabform.gitlab.PythonGitlab")
77-
def test_extra_kwargs_are_passed_to_python_gitlab(self, mock_python_gitlab):
78-
"""Test that extra kwargs from config are passed to PythonGitlab initialization"""
79-
# Setup mock GitLab object with extra config parameters
112+
def test_only_valid_gitlab_client_params_passed(self, mock_python_gitlab, mock_graphql):
113+
"""Test that only valid Gitlab client parameters are passed"""
80114
mock_gitlab = MagicMock(spec=GitLab)
81115
mock_gitlab.session = MagicMock()
82116
mock_gitlab.gitlab_config = {
83117
"url": "https://gitlab.example.com",
84118
"token": "test-token",
85119
"ssl_verify": True,
86120
"timeout": 10,
87-
"retry_transient_errors": True,
88-
"obey_rate_limit": True,
89-
"foo": "bar", # extra param for testing
121+
"max_retries": 5, # GraphQL only
122+
"obey_rate_limit": True, # GraphQL only
123+
"some_unknown_param": "value", # Should be filtered out
90124
}
91125

92-
# Create wrapper
93126
_ = GitlabWrapper(mock_gitlab)
94127

95-
# Verify PythonGitlab was called with the correct arguments
96-
mock_python_gitlab.assert_called_once()
97128
call_kwargs = mock_python_gitlab.call_args.kwargs
98129

99-
# Check standard parameters
130+
# Valid params should be present
100131
assert call_kwargs["url"] == "https://gitlab.example.com"
101132
assert call_kwargs["private_token"] == "test-token"
102133
assert call_kwargs["ssl_verify"] is True
103134
assert call_kwargs["timeout"] == 10
104-
assert call_kwargs["api_version"] == "4"
105135

106-
# Check extra kwargs are passed
136+
# Invalid params should NOT be present
137+
assert "max_retries" not in call_kwargs
138+
assert "obey_rate_limit" not in call_kwargs
139+
assert "some_unknown_param" not in call_kwargs
140+
141+
@patch("gitlabform.gitlab.GraphQL")
142+
@patch("gitlabform.gitlab.PythonGitlab")
143+
def test_graphql_params_passed_correctly(self, mock_python_gitlab, mock_graphql):
144+
"""Test that GraphQL-specific parameters are passed to GraphQL client"""
145+
mock_gitlab = MagicMock(spec=GitLab)
146+
mock_gitlab.session = MagicMock()
147+
mock_gitlab.gitlab_config = {
148+
"url": "https://gitlab.example.com",
149+
"token": "test-token",
150+
"max_retries": 5,
151+
"obey_rate_limit": False,
152+
"retry_transient_errors": True,
153+
"timeout": 30,
154+
"ssl_verify": False,
155+
}
156+
157+
_ = GitlabWrapper(mock_gitlab)
158+
159+
call_kwargs = mock_graphql.call_args.kwargs
160+
161+
assert call_kwargs["max_retries"] == 5
162+
assert call_kwargs["obey_rate_limit"] is False
107163
assert call_kwargs["retry_transient_errors"] is True
108-
assert call_kwargs["obey_rate_limit"] is True
109-
assert call_kwargs["foo"] == "bar"
164+
assert call_kwargs["timeout"] == 30
165+
assert call_kwargs["ssl_verify"] is False
166+
167+
@patch("gitlabform.gitlab.GraphQL")
168+
@patch("gitlabform.gitlab.PythonGitlab")
169+
def test_token_renamed_to_private_token(self, mock_python_gitlab, mock_graphql):
170+
"""Test that 'token' config key is renamed to 'private_token' for Gitlab client"""
171+
mock_gitlab = MagicMock(spec=GitLab)
172+
mock_gitlab.session = MagicMock()
173+
mock_gitlab.gitlab_config = {
174+
"url": "https://gitlab.example.com",
175+
"token": "my-secret-token",
176+
}
177+
178+
_ = GitlabWrapper(mock_gitlab)
179+
180+
call_kwargs = mock_python_gitlab.call_args.kwargs
181+
182+
assert "token" not in call_kwargs
183+
assert call_kwargs["private_token"] == "my-secret-token"
110184

111185
@patch("gitlabform.gitlab.GraphQL")
112186
@patch("gitlabform.gitlab.PythonGitlab")
113-
def test_default_values_for_gitlab_config(self, mock_python_gitlab, mock_graphql):
114-
"""Test that when only standard keys are provided, default values are used for others"""
187+
def test_retry_transient_errors_default_true(self, mock_python_gitlab, mock_graphql):
188+
"""Test that retry_transient_errors defaults to True for Gitlab client"""
115189
mock_gitlab = MagicMock(spec=GitLab)
116190
mock_gitlab.session = MagicMock()
117191
mock_gitlab.gitlab_config = {
@@ -122,17 +196,39 @@ def test_default_values_for_gitlab_config(self, mock_python_gitlab, mock_graphql
122196
_ = GitlabWrapper(mock_gitlab)
123197

124198
call_kwargs = mock_python_gitlab.call_args.kwargs
199+
assert call_kwargs["retry_transient_errors"] is True
125200

126-
print(call_kwargs)
127-
# Verify standard keys are passed directly as named parameters with defaults for others
128-
assert call_kwargs["url"] == "https://gitlab.example.com"
129-
assert call_kwargs["private_token"] == "test-token"
130-
assert call_kwargs["retry_transient_errors"] is True # default value of kwarg
201+
@patch("gitlabform.gitlab.GraphQL")
202+
@patch("gitlabform.gitlab.PythonGitlab")
203+
def test_retry_transient_errors_can_be_overridden(self, mock_python_gitlab, mock_graphql):
204+
"""Test that retry_transient_errors can be overridden from config"""
205+
mock_gitlab = MagicMock(spec=GitLab)
206+
mock_gitlab.session = MagicMock()
207+
mock_gitlab.gitlab_config = {
208+
"url": "https://gitlab.example.com",
209+
"token": "test-token",
210+
"retry_transient_errors": False,
211+
}
131212

132-
# Verify no extra kwargs beyond the expected parameters
133-
expected_keys = {"url", "private_token", "retry_transient_errors", "api_version", "graphql", "session"}
134-
actual_keys = set(call_kwargs.keys())
135-
extra_keys = actual_keys - expected_keys
213+
_ = GitlabWrapper(mock_gitlab)
214+
215+
call_kwargs = mock_python_gitlab.call_args.kwargs
216+
assert call_kwargs["retry_transient_errors"] is False
217+
218+
@patch("gitlabform.gitlab.GraphQL")
219+
@patch("gitlabform.gitlab.PythonGitlab")
220+
def test_graphql_url_and_token_passed(self, mock_python_gitlab, mock_graphql):
221+
"""Test that URL and token are passed to GraphQL client"""
222+
mock_gitlab = MagicMock(spec=GitLab)
223+
mock_gitlab.session = MagicMock()
224+
mock_gitlab.gitlab_config = {
225+
"url": "https://gitlab.example.com",
226+
"token": "test-token",
227+
}
228+
229+
_ = GitlabWrapper(mock_gitlab)
136230

137-
# Only extra keys should be those not in excluded_keys (should be empty in this case)
138-
assert len(extra_keys) == 0
231+
mock_graphql.assert_called_once()
232+
call_args = mock_graphql.call_args
233+
assert call_args.kwargs["url"] == "https://gitlab.example.com"
234+
assert call_args.kwargs["token"] == "test-token"

0 commit comments

Comments
 (0)