Skip to content

Commit 8da9421

Browse files
fix: use the [] after key names for array variables
1. If a value is of type ArrayAttribute then append '[]' to the name of the value. 2. 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. This is step 3 in a series of steps of our goal to add full support for the GitLab API data types[1]: * array * hash * array of hashes Step one was: commit 5127b15 Step two was: commit a57334f Fixes: #1698 [1] https://docs.gitlab.com/ee/api/#encoding-api-parameters-of-array-and-hash-types
1 parent 0ab0fc1 commit 8da9421

File tree

7 files changed

+109
-7
lines changed

7 files changed

+109
-7
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

+18-1
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:
@@ -31,6 +31,9 @@ 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+
3437

3538
class _ListArrayAttribute(GitlabAttribute):
3639
"""Helper class to support `list` / `array` types."""
@@ -55,17 +58,31 @@ class ArrayAttribute(_ListArrayAttribute):
5558
"""To support `array` types as documented in
5659
https://docs.gitlab.com/ee/api/#array"""
5760

61+
def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, str]]:
62+
if isinstance(self._value, str):
63+
return [(f"{key}[]", self._value)]
64+
65+
if TYPE_CHECKING:
66+
assert isinstance(self._value, list)
67+
return [(f"{key}[]", str(value)) for value in self._value]
68+
5869

5970
class CommaSeparatedListAttribute(_ListArrayAttribute):
6071
"""For values which are sent to the server as a Comma Separated Values
6172
(CSV) string. We allow them to be specified as a list and we convert it
6273
into a CSV"""
6374

75+
def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, str]]:
76+
return [(key, self.get_for_api())]
77+
6478

6579
class LowercaseStringAttribute(GitlabAttribute):
6680
def get_for_api(self) -> str:
6781
return str(self._value).lower()
6882

83+
def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, str]]:
84+
return [(key, self.get_for_api())]
85+
6986

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

gitlab/utils.py

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ def response_content(
4747
def copy_dict(dest: Dict[str, Any], src: Dict[str, Any]) -> None:
4848
for k, v in src.items():
4949
if isinstance(v, dict):
50+
# NOTE(jlvillal): This provides some support for the `hash` type
51+
# https://docs.gitlab.com/ee/api/#hash
5052
# Transform dict values to new attributes. For example:
5153
# custom_attributes: {'foo', 'bar'} =>
5254
# "custom_attributes['foo']": "bar"

tests/functional/api/test_groups.py

+5
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ def test_groups(gl):
9191
assert len(group1.members.list()) == 3
9292
assert len(group2.members.list()) == 2
9393

94+
# Test `user_ids` array
95+
result = group1.members.list(user_ids=[user.id, 99999])
96+
assert len(result) == 1
97+
assert result[0].id == user.id
98+
9499
group1.members.delete(user.id)
95100
assert len(group1.members.list()) == 2
96101
assert len(group1.members_all.list())

tests/unit/test_gitlab_http_methods.py

+18
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import requests
33
import responses
44

5+
import gitlab
56
from gitlab import GitlabHttpError, GitlabList, GitlabParsingError, RedirectError
67
from tests.unit import helpers
78

@@ -507,3 +508,20 @@ def test_delete_request_404(gl):
507508
with pytest.raises(GitlabHttpError):
508509
gl.http_delete("/not_there")
509510
assert responses.assert_call_count(url, 1) is True
511+
512+
513+
@responses.activate
514+
def test_array_type_request(gl):
515+
url = "http://localhost/api/v4/projects"
516+
params = "array_var[]=1&array_var[]=2&array_var[]=3"
517+
full_url = f"{url}?array_var%5B%5D=1&array_var%5B%5D=2&array_var%5B%5D=3"
518+
responses.add(
519+
method=responses.GET,
520+
url=url,
521+
json={"name": "project1"},
522+
status=200,
523+
match=[responses.matchers.query_string_matcher(params)],
524+
)
525+
526+
gl.http_get("/projects", array_var=gitlab.types.ArrayAttribute([1, 2, 3]))
527+
assert responses.assert_call_count(full_url, 1) is True

tests/unit/test_types.py

+34
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def test_gitlab_attribute_get():
2525
o.set_from_cli("whatever2")
2626
assert o.get() == "whatever2"
2727
assert o.get_for_api() == "whatever2"
28+
assert o.get_as_tuple_list(key="foo") == [("foo", "whatever2")]
2829

2930
o = types.GitlabAttribute()
3031
assert o._value is None
@@ -64,6 +65,24 @@ def test_array_attribute_get_for_api_from_int_list():
6465
assert o.get_for_api() == "1,9,7"
6566

6667

68+
def test_array_attribute_get_as_tuple_list_from_list():
69+
o = types.ArrayAttribute(["foo", "bar", "baz"])
70+
assert o.get_as_tuple_list(key="identifier") == [
71+
("identifier[]", "foo"),
72+
("identifier[]", "bar"),
73+
("identifier[]", "baz"),
74+
]
75+
76+
77+
def test_array_attribute_get_as_tuple_list_from_int_list():
78+
o = types.ArrayAttribute([1, 9, 7])
79+
assert o.get_as_tuple_list(key="identifier") == [
80+
("identifier[]", "1"),
81+
("identifier[]", "9"),
82+
("identifier[]", "7"),
83+
]
84+
85+
6786
def test_array_attribute_does_not_split_string():
6887
o = types.ArrayAttribute("foo")
6988
assert o.get_for_api() == "foo"
@@ -86,7 +105,22 @@ def test_csv_string_attribute_get_for_api_from_int_list():
86105
assert o.get_for_api() == "1,9,7"
87106

88107

108+
def test_csv_string_attribute_get_as_tuple_list_from_list():
109+
o = types.CommaSeparatedListAttribute(["foo", "bar", "baz"])
110+
assert o.get_as_tuple_list(key="identifier") == [("identifier", "foo,bar,baz")]
111+
112+
113+
def test_csv_string_attribute_get_as_tuple_list_from_int_list():
114+
o = types.CommaSeparatedListAttribute([1, 9, 7])
115+
assert o.get_as_tuple_list(key="identifier") == [("identifier", "1,9,7")]
116+
117+
89118
# LowercaseStringAttribute tests
90119
def test_lowercase_string_attribute_get_for_api():
91120
o = types.LowercaseStringAttribute("FOO")
92121
assert o.get_for_api() == "foo"
122+
123+
124+
def test_lowercase_string_attribute_get_as_tuple():
125+
o = types.LowercaseStringAttribute("FOO")
126+
assert o.get_as_tuple_list(key="user_name") == [("user_name", "foo")]

0 commit comments

Comments
 (0)