Skip to content

Commit 510ec30

Browse files
authored
Merge pull request #1699 from python-gitlab/jlvillal/arrays
fix: use the [] after key names for array variables in `params`
2 parents 194ee01 + 1af44ce commit 510ec30

File tree

7 files changed

+134
-31
lines changed

7 files changed

+134
-31
lines changed

gitlab/mixins.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,12 @@ def list(self, **kwargs: Any) -> Union[base.RESTObjectList, List[base.RESTObject
238238
GitlabListError: If the server cannot perform the request
239239
"""
240240

241-
data, _ = utils._transform_types(kwargs, self._types, transform_files=False)
241+
data, _ = utils._transform_types(
242+
data=kwargs,
243+
custom_types=self._types,
244+
transform_data=True,
245+
transform_files=False,
246+
)
242247

243248
if self.gitlab.per_page:
244249
data.setdefault("per_page", self.gitlab.per_page)
@@ -303,7 +308,9 @@ def create(
303308
data = {}
304309

305310
self._create_attrs.validate_attrs(data=data)
306-
data, files = utils._transform_types(data, self._types)
311+
data, files = utils._transform_types(
312+
data=data, custom_types=self._types, transform_data=False
313+
)
307314

308315
# Handle specific URL for creation
309316
path = kwargs.pop("path", self.path)
@@ -370,7 +377,9 @@ def update(
370377
if self._obj_cls is not None and self._obj_cls._id_attr is not None:
371378
excludes = [self._obj_cls._id_attr]
372379
self._update_attrs.validate_attrs(data=new_data, excludes=excludes)
373-
new_data, files = utils._transform_types(new_data, self._types)
380+
new_data, files = utils._transform_types(
381+
data=new_data, custom_types=self._types, transform_data=False
382+
)
374383

375384
http_method = self._get_update_method()
376385
result = http_method(path, post_data=new_data, files=files, **kwargs)

gitlab/types.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ def get(self) -> Any:
6363
def set_from_cli(self, cli_value: Any) -> None:
6464
self._value = cli_value
6565

66-
def get_for_api(self) -> Any:
67-
return self._value
66+
def get_for_api(self, *, key: str) -> Tuple[str, Any]:
67+
return (key, self._value)
6868

6969

7070
class _ListArrayAttribute(GitlabAttribute):
@@ -76,20 +76,28 @@ def set_from_cli(self, cli_value: str) -> None:
7676
else:
7777
self._value = [item.strip() for item in cli_value.split(",")]
7878

79-
def get_for_api(self) -> str:
79+
def get_for_api(self, *, key: str) -> Tuple[str, str]:
8080
# Do not comma-split single value passed as string
8181
if isinstance(self._value, str):
82-
return self._value
82+
return (key, self._value)
8383

8484
if TYPE_CHECKING:
8585
assert isinstance(self._value, list)
86-
return ",".join([str(x) for x in self._value])
86+
return (key, ",".join([str(x) for x in self._value]))
8787

8888

8989
class ArrayAttribute(_ListArrayAttribute):
9090
"""To support `array` types as documented in
9191
https://docs.gitlab.com/ee/api/#array"""
9292

93+
def get_for_api(self, *, key: str) -> Tuple[str, Any]:
94+
if isinstance(self._value, str):
95+
return (f"{key}[]", self._value)
96+
97+
if TYPE_CHECKING:
98+
assert isinstance(self._value, list)
99+
return (f"{key}[]", self._value)
100+
93101

94102
class CommaSeparatedListAttribute(_ListArrayAttribute):
95103
"""For values which are sent to the server as a Comma Separated Values
@@ -98,8 +106,8 @@ class CommaSeparatedListAttribute(_ListArrayAttribute):
98106

99107

100108
class LowercaseStringAttribute(GitlabAttribute):
101-
def get_for_api(self) -> str:
102-
return str(self._value).lower()
109+
def get_for_api(self, *, key: str) -> Tuple[str, str]:
110+
return (key, str(self._value).lower())
103111

104112

105113
class FileAttribute(GitlabAttribute):

gitlab/utils.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,34 +55,53 @@ def response_content(
5555

5656

5757
def _transform_types(
58-
data: Dict[str, Any], custom_types: dict, *, transform_files: Optional[bool] = True
58+
data: Dict[str, Any],
59+
custom_types: dict,
60+
*,
61+
transform_data: bool,
62+
transform_files: Optional[bool] = True,
5963
) -> Tuple[dict, dict]:
6064
"""Copy the data dict with attributes that have custom types and transform them
6165
before being sent to the server.
6266
63-
If ``transform_files`` is ``True`` (default), also populates the ``files`` dict for
67+
``transform_files``: If ``True`` (default), also populates the ``files`` dict for
6468
FileAttribute types with tuples to prepare fields for requests' MultipartEncoder:
6569
https://toolbelt.readthedocs.io/en/latest/user.html#multipart-form-data-encoder
6670
71+
``transform_data``: If ``True`` transforms the ``data`` dict with fields
72+
suitable for encoding as query parameters for GitLab's API:
73+
https://docs.gitlab.com/ee/api/#encoding-api-parameters-of-array-and-hash-types
74+
6775
Returns:
6876
A tuple of the transformed data dict and files dict"""
6977

7078
# Duplicate data to avoid messing with what the user sent us
7179
data = data.copy()
80+
if not transform_files and not transform_data:
81+
return data, {}
82+
7283
files = {}
7384

74-
for attr_name, type_cls in custom_types.items():
85+
for attr_name, attr_class in custom_types.items():
7586
if attr_name not in data:
7687
continue
7788

78-
type_obj = type_cls(data[attr_name])
89+
gitlab_attribute = attr_class(data[attr_name])
7990

80-
# if the type if FileAttribute we need to pass the data as file
81-
if transform_files and isinstance(type_obj, types.FileAttribute):
82-
key = type_obj.get_file_name(attr_name)
91+
# if the type is FileAttribute we need to pass the data as file
92+
if isinstance(gitlab_attribute, types.FileAttribute) and transform_files:
93+
key = gitlab_attribute.get_file_name(attr_name)
8394
files[attr_name] = (key, data.pop(attr_name))
84-
else:
85-
data[attr_name] = type_obj.get_for_api()
95+
continue
96+
97+
if not transform_data:
98+
continue
99+
100+
if isinstance(gitlab_attribute, types.GitlabAttribute):
101+
key, value = gitlab_attribute.get_for_api(key=attr_name)
102+
if key != attr_name:
103+
del data[attr_name]
104+
data[key] = value
86105

87106
return data, files
88107

@@ -94,6 +113,8 @@ def copy_dict(
94113
) -> None:
95114
for k, v in src.items():
96115
if isinstance(v, dict):
116+
# NOTE(jlvillal): This provides some support for the `hash` type
117+
# https://docs.gitlab.com/ee/api/#hash
97118
# Transform dict values to new attributes. For example:
98119
# custom_attributes: {'foo', 'bar'} =>
99120
# "custom_attributes['foo']": "bar"

tests/functional/api/test_groups.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ def test_groups(gl):
9999
assert len(group1.members.list()) == 3
100100
assert len(group2.members.list()) == 2
101101

102+
# Test `user_ids` array
103+
result = group1.members.list(user_ids=[user.id, 99999])
104+
assert len(result) == 1
105+
assert result[0].id == user.id
106+
102107
group1.members.delete(user.id)
103108
assert user not in group1.members.list()
104109
assert group1.members_all.list()

tests/unit/mixins/test_mixin_methods.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,25 @@ class M(ListMixin, FakeManager):
205205
assert responses.assert_call_count(url, 2) is True
206206

207207

208+
@responses.activate
209+
def test_list_mixin_with_attributes(gl):
210+
class M(ListMixin, FakeManager):
211+
_types = {"my_array": gl_types.ArrayAttribute}
212+
213+
url = "http://localhost/api/v4/tests"
214+
responses.add(
215+
method=responses.GET,
216+
headers={},
217+
url=url,
218+
json=[],
219+
status=200,
220+
match=[responses.matchers.query_param_matcher({"my_array[]": ["1", "2", "3"]})],
221+
)
222+
223+
mgr = M(gl)
224+
mgr.list(iterator=True, my_array=[1, 2, 3])
225+
226+
208227
@responses.activate
209228
def test_list_other_url(gl):
210229
class M(ListMixin, FakeManager):
@@ -295,6 +314,25 @@ class M(CreateMixin, FakeManager):
295314
assert responses.assert_call_count(url, 1) is True
296315

297316

317+
@responses.activate
318+
def test_create_mixin_with_attributes(gl):
319+
class M(CreateMixin, FakeManager):
320+
_types = {"my_array": gl_types.ArrayAttribute}
321+
322+
url = "http://localhost/api/v4/tests"
323+
responses.add(
324+
method=responses.POST,
325+
headers={},
326+
url=url,
327+
json={},
328+
status=200,
329+
match=[responses.matchers.json_params_matcher({"my_array": [1, 2, 3]})],
330+
)
331+
332+
mgr = M(gl)
333+
mgr.create({"my_array": [1, 2, 3]})
334+
335+
298336
def test_update_mixin_missing_attrs(gl):
299337
class M(UpdateMixin, FakeManager):
300338
_update_attrs = gl_types.RequiredOptional(

tests/unit/test_types.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def test_gitlab_attribute_get():
7373

7474
o.set_from_cli("whatever2")
7575
assert o.get() == "whatever2"
76-
assert o.get_for_api() == "whatever2"
76+
assert o.get_for_api(key="spam") == ("spam", "whatever2")
7777

7878
o = types.GitlabAttribute()
7979
assert o._value is None
@@ -100,42 +100,42 @@ def test_array_attribute_empty_input():
100100
def test_array_attribute_get_for_api_from_cli():
101101
o = types.ArrayAttribute()
102102
o.set_from_cli("foo,bar,baz")
103-
assert o.get_for_api() == "foo,bar,baz"
103+
assert o.get_for_api(key="spam") == ("spam[]", ["foo", "bar", "baz"])
104104

105105

106106
def test_array_attribute_get_for_api_from_list():
107107
o = types.ArrayAttribute(["foo", "bar", "baz"])
108-
assert o.get_for_api() == "foo,bar,baz"
108+
assert o.get_for_api(key="spam") == ("spam[]", ["foo", "bar", "baz"])
109109

110110

111111
def test_array_attribute_get_for_api_from_int_list():
112112
o = types.ArrayAttribute([1, 9, 7])
113-
assert o.get_for_api() == "1,9,7"
113+
assert o.get_for_api(key="spam") == ("spam[]", [1, 9, 7])
114114

115115

116116
def test_array_attribute_does_not_split_string():
117117
o = types.ArrayAttribute("foo")
118-
assert o.get_for_api() == "foo"
118+
assert o.get_for_api(key="spam") == ("spam[]", "foo")
119119

120120

121121
# CommaSeparatedListAttribute tests
122122
def test_csv_string_attribute_get_for_api_from_cli():
123123
o = types.CommaSeparatedListAttribute()
124124
o.set_from_cli("foo,bar,baz")
125-
assert o.get_for_api() == "foo,bar,baz"
125+
assert o.get_for_api(key="spam") == ("spam", "foo,bar,baz")
126126

127127

128128
def test_csv_string_attribute_get_for_api_from_list():
129129
o = types.CommaSeparatedListAttribute(["foo", "bar", "baz"])
130-
assert o.get_for_api() == "foo,bar,baz"
130+
assert o.get_for_api(key="spam") == ("spam", "foo,bar,baz")
131131

132132

133133
def test_csv_string_attribute_get_for_api_from_int_list():
134134
o = types.CommaSeparatedListAttribute([1, 9, 7])
135-
assert o.get_for_api() == "1,9,7"
135+
assert o.get_for_api(key="spam") == ("spam", "1,9,7")
136136

137137

138138
# LowercaseStringAttribute tests
139139
def test_lowercase_string_attribute_get_for_api():
140140
o = types.LowercaseStringAttribute("FOO")
141-
assert o.get_for_api() == "foo"
141+
assert o.get_for_api(key="spam") == ("spam", "foo")

tests/unit/test_utils.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def test_remove_none_from_dict(dictionary, expected):
155155

156156
def test_transform_types_copies_data_with_empty_files():
157157
data = {"attr": "spam"}
158-
new_data, files = utils._transform_types(data, {})
158+
new_data, files = utils._transform_types(data, {}, transform_data=True)
159159

160160
assert new_data is not data
161161
assert new_data == data
@@ -165,7 +165,7 @@ def test_transform_types_copies_data_with_empty_files():
165165
def test_transform_types_with_transform_files_populates_files():
166166
custom_types = {"attr": types.FileAttribute}
167167
data = {"attr": "spam"}
168-
new_data, files = utils._transform_types(data, custom_types)
168+
new_data, files = utils._transform_types(data, custom_types, transform_data=True)
169169

170170
assert new_data == {}
171171
assert files["attr"] == ("attr", "spam")
@@ -174,7 +174,29 @@ def test_transform_types_with_transform_files_populates_files():
174174
def test_transform_types_without_transform_files_populates_data_with_empty_files():
175175
custom_types = {"attr": types.FileAttribute}
176176
data = {"attr": "spam"}
177-
new_data, files = utils._transform_types(data, custom_types, transform_files=False)
177+
new_data, files = utils._transform_types(
178+
data, custom_types, transform_files=False, transform_data=True
179+
)
178180

179181
assert new_data == {"attr": "spam"}
180182
assert files == {}
183+
184+
185+
def test_transform_types_params_array():
186+
data = {"attr": [1, 2, 3]}
187+
custom_types = {"attr": types.ArrayAttribute}
188+
new_data, files = utils._transform_types(data, custom_types, transform_data=True)
189+
190+
assert new_data is not data
191+
assert new_data == {"attr[]": [1, 2, 3]}
192+
assert files == {}
193+
194+
195+
def test_transform_types_not_params_array():
196+
data = {"attr": [1, 2, 3]}
197+
custom_types = {"attr": types.ArrayAttribute}
198+
new_data, files = utils._transform_types(data, custom_types, transform_data=False)
199+
200+
assert new_data is not data
201+
assert new_data == data
202+
assert files == {}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy