Skip to content

Commit e0554b9

Browse files
fix: use the [] after key names for array variables
1. Create a new CommaSeparatedListAttribute class. This is to indicate types which are sent to the GitLab server as comma-separated-strings (CSV) but we have been allowing users to use a list-of-strings. These values are NOT array values, so adding [] to the key name breaks them. 2. Rename ListAttribute to ArrayAttribute. 3. If a value is of type ArrayAttribute then append '[]' to the name of the value. 4. Move processing of most GitlabAttributes into the client.py:http_request() method. Now we convert our params into a list of tuples so that we can have multiple identical keys but with different values. Fixes: #1698
1 parent ff4b1cc commit e0554b9

15 files changed

+178
-55
lines changed

gitlab/client.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
"""Wrapper for the GitLab API."""
1818

19+
import copy
1920
import os
2021
import time
2122
from typing import Any, cast, Dict, List, Optional, Tuple, TYPE_CHECKING, Union
@@ -27,6 +28,7 @@
2728
import gitlab.config
2829
import gitlab.const
2930
import gitlab.exceptions
31+
from gitlab import types as gl_types
3032
from gitlab import utils
3133

3234
REDIRECT_MSG = (
@@ -604,6 +606,28 @@ def _prepare_send_data(
604606

605607
return (post_data, None, "application/json")
606608

609+
@staticmethod
610+
def _prepare_dict_for_api(*, in_dict: Dict[str, Any]) -> Dict[str, Any]:
611+
result: Dict[str, Any] = {}
612+
for key, value in in_dict.items():
613+
if isinstance(value, gl_types.GitlabAttribute):
614+
result[key] = value.get_for_api()
615+
else:
616+
result[key] = copy.deepcopy(in_dict[key])
617+
return result
618+
619+
@staticmethod
620+
def _param_dict_to_param_tuples(*, params: Dict[str, Any]) -> List[Tuple[str, Any]]:
621+
"""Convert a dict to a list of key/values. This will be used to pass
622+
values to requests"""
623+
result: List[Tuple[str, Any]] = []
624+
for key, value in params.items():
625+
if isinstance(value, gl_types.GitlabAttribute):
626+
result.extend(value.get_as_tuple_list(key=key))
627+
else:
628+
result.append((key, value))
629+
return result
630+
607631
def http_request(
608632
self,
609633
verb: str,
@@ -663,6 +687,10 @@ def http_request(
663687
else:
664688
utils.copy_dict(params, kwargs)
665689

690+
tuple_params = self._param_dict_to_param_tuples(params=params)
691+
if isinstance(post_data, dict):
692+
post_data = self._prepare_dict_for_api(in_dict=post_data)
693+
666694
opts = self._get_session_opts()
667695

668696
verify = opts.pop("verify")
@@ -682,7 +710,7 @@ def http_request(
682710
url=url,
683711
json=json,
684712
data=data,
685-
params=params,
713+
params=tuple_params,
686714
timeout=timeout,
687715
verify=verify,
688716
stream=streamed,

gitlab/mixins.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,7 @@ def list(self, **kwargs: Any) -> Union[base.RESTObjectList, List[base.RESTObject
230230
if self._types:
231231
for attr_name, type_cls in self._types.items():
232232
if attr_name in data.keys():
233-
type_obj = type_cls(data[attr_name])
234-
data[attr_name] = type_obj.get_for_api()
233+
data[attr_name] = type_cls(data[attr_name])
235234

236235
# Allow to overwrite the path, handy for custom listings
237236
path = data.pop("path", self.path)
@@ -307,14 +306,13 @@ def create(
307306
for attr_name, type_cls in self._types.items():
308307
if attr_name in data.keys():
309308
type_obj = type_cls(data[attr_name])
310-
311309
# if the type if FileAttribute we need to pass the data as
312310
# file
313311
if isinstance(type_obj, g_types.FileAttribute):
314312
k = type_obj.get_file_name(attr_name)
315313
files[attr_name] = (k, data.pop(attr_name))
316314
else:
317-
data[attr_name] = type_obj.get_for_api()
315+
data[attr_name] = type_obj
318316

319317
# Handle specific URL for creation
320318
path = kwargs.pop("path", self.path)
@@ -410,7 +408,7 @@ def update(
410408
k = type_obj.get_file_name(attr_name)
411409
files[attr_name] = (k, new_data.pop(attr_name))
412410
else:
413-
new_data[attr_name] = type_obj.get_for_api()
411+
new_data[attr_name] = type_obj
414412

415413
http_method = self._get_update_method()
416414
result = http_method(path, post_data=new_data, files=files, **kwargs)

gitlab/types.py

+43-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# You should have received a copy of the GNU Lesser General Public License
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717

18-
from typing import Any, Optional, TYPE_CHECKING
18+
from typing import Any, List, Optional, Tuple, TYPE_CHECKING
1919

2020

2121
class GitlabAttribute(object):
@@ -31,8 +31,43 @@ def set_from_cli(self, cli_value: Any) -> None:
3131
def get_for_api(self) -> Any:
3232
return self._value
3333

34+
def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, Any]]:
35+
return [(key, self._value)]
36+
37+
38+
class ArrayAttribute(GitlabAttribute):
39+
"""To support `array` types as documented in
40+
https://docs.gitlab.com/ee/api/#array"""
41+
42+
def set_from_cli(self, cli_value: str) -> None:
43+
if not cli_value.strip():
44+
self._value = []
45+
else:
46+
self._value = [item.strip() for item in cli_value.split(",")]
47+
48+
def get_for_api(self) -> str:
49+
# Do not comma-split single value passed as string
50+
if isinstance(self._value, str):
51+
return self._value
52+
53+
if TYPE_CHECKING:
54+
assert isinstance(self._value, list)
55+
return ",".join([str(x) for x in self._value])
56+
57+
def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, str]]:
58+
if isinstance(self._value, str):
59+
return [(f"{key}[]", self._value)]
60+
61+
if TYPE_CHECKING:
62+
assert isinstance(self._value, list)
63+
return [(f"{key}[]", str(value)) for value in self._value]
64+
65+
66+
class CommaSeparatedListAttribute(GitlabAttribute):
67+
"""For values which are sent to the server as a Comma Separated Values
68+
(CSV) string. We allow them to be specified as a list and we convert it
69+
into a CSV"""
3470

35-
class ListAttribute(GitlabAttribute):
3671
def set_from_cli(self, cli_value: str) -> None:
3772
if not cli_value.strip():
3873
self._value = []
@@ -48,11 +83,17 @@ def get_for_api(self) -> str:
4883
assert isinstance(self._value, list)
4984
return ",".join([str(x) for x in self._value])
5085

86+
def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, str]]:
87+
return [(key, self.get_for_api())]
88+
5189

5290
class LowercaseStringAttribute(GitlabAttribute):
5391
def get_for_api(self) -> str:
5492
return str(self._value).lower()
5593

94+
def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, str]]:
95+
return [(key, self.get_for_api())]
96+
5697

5798
class FileAttribute(GitlabAttribute):
5899
def get_file_name(self, attr_name: Optional[str] = None) -> Optional[str]:

gitlab/v4/objects/deploy_tokens.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class GroupDeployTokenManager(ListMixin, CreateMixin, DeleteMixin, RESTManager):
3939
"username",
4040
),
4141
)
42-
_types = {"scopes": types.ListAttribute}
42+
_types = {"scopes": types.CommaSeparatedListAttribute}
4343

4444

4545
class ProjectDeployToken(ObjectDeleteMixin, RESTObject):
@@ -60,4 +60,4 @@ class ProjectDeployTokenManager(ListMixin, CreateMixin, DeleteMixin, RESTManager
6060
"username",
6161
),
6262
)
63-
_types = {"scopes": types.ListAttribute}
63+
_types = {"scopes": types.CommaSeparatedListAttribute}

gitlab/v4/objects/epics.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class GroupEpicManager(CRUDMixin, RESTManager):
4242
_update_attrs = RequiredOptional(
4343
optional=("title", "labels", "description", "start_date", "end_date"),
4444
)
45-
_types = {"labels": types.ListAttribute}
45+
_types = {"labels": types.CommaSeparatedListAttribute}
4646

4747
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> GroupEpic:
4848
return cast(GroupEpic, super().get(id=id, lazy=lazy, **kwargs))

gitlab/v4/objects/groups.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ class GroupManager(CRUDMixin, RESTManager):
314314
"shared_runners_setting",
315315
),
316316
)
317-
_types = {"avatar": types.ImageAttribute, "skip_groups": types.ListAttribute}
317+
_types = {"avatar": types.ImageAttribute, "skip_groups": types.ArrayAttribute}
318318

319319
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Group:
320320
return cast(Group, super().get(id=id, lazy=lazy, **kwargs))
@@ -374,7 +374,7 @@ class GroupSubgroupManager(ListMixin, RESTManager):
374374
"with_custom_attributes",
375375
"min_access_level",
376376
)
377-
_types = {"skip_groups": types.ListAttribute}
377+
_types = {"skip_groups": types.ArrayAttribute}
378378

379379

380380
class GroupDescendantGroup(RESTObject):

gitlab/v4/objects/issues.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class IssueManager(RetrieveMixin, RESTManager):
6565
"updated_after",
6666
"updated_before",
6767
)
68-
_types = {"iids": types.ListAttribute, "labels": types.ListAttribute}
68+
_types = {"iids": types.ArrayAttribute, "labels": types.CommaSeparatedListAttribute}
6969

7070
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Issue:
7171
return cast(Issue, super().get(id=id, lazy=lazy, **kwargs))
@@ -95,7 +95,7 @@ class GroupIssueManager(ListMixin, RESTManager):
9595
"updated_after",
9696
"updated_before",
9797
)
98-
_types = {"iids": types.ListAttribute, "labels": types.ListAttribute}
98+
_types = {"iids": types.ArrayAttribute, "labels": types.CommaSeparatedListAttribute}
9999

100100

101101
class ProjectIssue(
@@ -233,7 +233,7 @@ class ProjectIssueManager(CRUDMixin, RESTManager):
233233
"discussion_locked",
234234
),
235235
)
236-
_types = {"iids": types.ListAttribute, "labels": types.ListAttribute}
236+
_types = {"iids": types.ArrayAttribute, "labels": types.CommaSeparatedListAttribute}
237237

238238
def get(
239239
self, id: Union[str, int], lazy: bool = False, **kwargs: Any

gitlab/v4/objects/members.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class GroupMemberManager(CRUDMixin, RESTManager):
4141
_update_attrs = RequiredOptional(
4242
required=("access_level",), optional=("expires_at",)
4343
)
44-
_types = {"user_ids": types.ListAttribute}
44+
_types = {"user_ids": types.ArrayAttribute}
4545

4646
def get(
4747
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
@@ -101,7 +101,7 @@ class ProjectMemberManager(CRUDMixin, RESTManager):
101101
_update_attrs = RequiredOptional(
102102
required=("access_level",), optional=("expires_at",)
103103
)
104-
_types = {"user_ids": types.ListAttribute}
104+
_types = {"user_ids": types.ArrayAttribute}
105105

106106
def get(
107107
self, id: Union[str, int], lazy: bool = False, **kwargs: Any

gitlab/v4/objects/merge_requests.py

+11-11
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,10 @@ class MergeRequestManager(ListMixin, RESTManager):
9595
"deployed_after",
9696
)
9797
_types = {
98-
"approver_ids": types.ListAttribute,
99-
"approved_by_ids": types.ListAttribute,
100-
"in": types.ListAttribute,
101-
"labels": types.ListAttribute,
98+
"approver_ids": types.ArrayAttribute,
99+
"approved_by_ids": types.ArrayAttribute,
100+
"in": types.CommaSeparatedListAttribute,
101+
"labels": types.CommaSeparatedListAttribute,
102102
}
103103

104104

@@ -133,9 +133,9 @@ class GroupMergeRequestManager(ListMixin, RESTManager):
133133
"wip",
134134
)
135135
_types = {
136-
"approver_ids": types.ListAttribute,
137-
"approved_by_ids": types.ListAttribute,
138-
"labels": types.ListAttribute,
136+
"approver_ids": types.ArrayAttribute,
137+
"approved_by_ids": types.ArrayAttribute,
138+
"labels": types.CommaSeparatedListAttribute,
139139
}
140140

141141

@@ -455,10 +455,10 @@ class ProjectMergeRequestManager(CRUDMixin, RESTManager):
455455
"wip",
456456
)
457457
_types = {
458-
"approver_ids": types.ListAttribute,
459-
"approved_by_ids": types.ListAttribute,
460-
"iids": types.ListAttribute,
461-
"labels": types.ListAttribute,
458+
"approver_ids": types.ArrayAttribute,
459+
"approved_by_ids": types.ArrayAttribute,
460+
"iids": types.ArrayAttribute,
461+
"labels": types.CommaSeparatedListAttribute,
462462
}
463463

464464
def get(

gitlab/v4/objects/milestones.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ class GroupMilestoneManager(CRUDMixin, RESTManager):
9393
optional=("title", "description", "due_date", "start_date", "state_event"),
9494
)
9595
_list_filters = ("iids", "state", "search")
96-
_types = {"iids": types.ListAttribute}
96+
_types = {"iids": types.ArrayAttribute}
9797

9898
def get(
9999
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
@@ -177,7 +177,7 @@ class ProjectMilestoneManager(CRUDMixin, RESTManager):
177177
optional=("title", "description", "due_date", "start_date", "state_event"),
178178
)
179179
_list_filters = ("iids", "state", "search")
180-
_types = {"iids": types.ListAttribute}
180+
_types = {"iids": types.ArrayAttribute}
181181

182182
def get(
183183
self, id: Union[str, int], lazy: bool = False, **kwargs: Any

gitlab/v4/objects/projects.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ class ProjectGroupManager(ListMixin, RESTManager):
125125
"shared_min_access_level",
126126
"shared_visible_only",
127127
)
128-
_types = {"skip_groups": types.ListAttribute}
128+
_types = {"skip_groups": types.ArrayAttribute}
129129

130130

131131
class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTObject):
@@ -807,7 +807,10 @@ class ProjectManager(CRUDMixin, RESTManager):
807807
"with_merge_requests_enabled",
808808
"with_programming_language",
809809
)
810-
_types = {"avatar": types.ImageAttribute, "topic": types.ListAttribute}
810+
_types = {
811+
"avatar": types.ImageAttribute,
812+
"topic": types.CommaSeparatedListAttribute,
813+
}
811814

812815
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Project:
813816
return cast(Project, super().get(id=id, lazy=lazy, **kwargs))

gitlab/v4/objects/runners.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ class RunnerManager(CRUDMixin, RESTManager):
6868
),
6969
)
7070
_list_filters = ("scope", "tag_list")
71-
_types = {"tag_list": types.ListAttribute}
71+
_types = {"tag_list": types.CommaSeparatedListAttribute}
7272

7373
@cli.register_custom_action("RunnerManager", tuple(), ("scope",))
7474
@exc.on_http_error(exc.GitlabListError)
@@ -130,7 +130,7 @@ class GroupRunnerManager(ListMixin, RESTManager):
130130
_from_parent_attrs = {"group_id": "id"}
131131
_create_attrs = RequiredOptional(required=("runner_id",))
132132
_list_filters = ("scope", "tag_list")
133-
_types = {"tag_list": types.ListAttribute}
133+
_types = {"tag_list": types.CommaSeparatedListAttribute}
134134

135135

136136
class ProjectRunner(ObjectDeleteMixin, RESTObject):
@@ -143,4 +143,4 @@ class ProjectRunnerManager(CreateMixin, DeleteMixin, ListMixin, RESTManager):
143143
_from_parent_attrs = {"project_id": "id"}
144144
_create_attrs = RequiredOptional(required=("runner_id",))
145145
_list_filters = ("scope", "tag_list")
146-
_types = {"tag_list": types.ListAttribute}
146+
_types = {"tag_list": types.CommaSeparatedListAttribute}

gitlab/v4/objects/settings.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,12 @@ class ApplicationSettingsManager(GetWithoutIdMixin, UpdateMixin, RESTManager):
8080
),
8181
)
8282
_types = {
83-
"asset_proxy_allowlist": types.ListAttribute,
84-
"disabled_oauth_sign_in_sources": types.ListAttribute,
85-
"domain_allowlist": types.ListAttribute,
86-
"domain_denylist": types.ListAttribute,
87-
"import_sources": types.ListAttribute,
88-
"restricted_visibility_levels": types.ListAttribute,
83+
"asset_proxy_allowlist": types.ArrayAttribute,
84+
"disabled_oauth_sign_in_sources": types.ArrayAttribute,
85+
"domain_allowlist": types.ArrayAttribute,
86+
"domain_denylist": types.ArrayAttribute,
87+
"import_sources": types.ArrayAttribute,
88+
"restricted_visibility_levels": types.ArrayAttribute,
8989
}
9090

9191
@exc.on_http_error(exc.GitlabUpdateError)

gitlab/v4/objects/users.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ class ProjectUserManager(ListMixin, RESTManager):
369369
_obj_cls = ProjectUser
370370
_from_parent_attrs = {"project_id": "id"}
371371
_list_filters = ("search", "skip_users")
372-
_types = {"skip_users": types.ListAttribute}
372+
_types = {"skip_users": types.ArrayAttribute}
373373

374374

375375
class UserEmail(ObjectDeleteMixin, RESTObject):

0 commit comments

Comments
 (0)