Skip to content

Commit a939db1

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 1ecbc7c commit a939db1

File tree

6 files changed

+111
-6
lines changed

6 files changed

+111
-6
lines changed

gitlab/client.py

Lines changed: 29 additions & 1 deletion
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
@@ -28,6 +29,7 @@
2829
import gitlab.config
2930
import gitlab.const
3031
import gitlab.exceptions
32+
from gitlab import types as gl_types
3133
from gitlab import utils
3234

3335
REDIRECT_MSG = (
@@ -626,6 +628,28 @@ def _prepare_send_data(
626628

627629
return (post_data, None, "application/json")
628630

631+
@staticmethod
632+
def _prepare_dict_for_api(*, in_dict: Dict[str, Any]) -> Dict[str, Any]:
633+
result: Dict[str, Any] = {}
634+
for key, value in in_dict.items():
635+
if isinstance(value, gl_types.GitlabAttribute):
636+
result[key] = value.get_for_api()
637+
else:
638+
result[key] = copy.deepcopy(in_dict[key])
639+
return result
640+
641+
@staticmethod
642+
def _param_dict_to_param_tuples(*, params: Dict[str, Any]) -> List[Tuple[str, Any]]:
643+
"""Convert a dict to a list of key/values. This will be used to pass
644+
values to requests"""
645+
result: List[Tuple[str, Any]] = []
646+
for key, value in params.items():
647+
if isinstance(value, gl_types.GitlabAttribute):
648+
result.extend(value.get_as_tuple_list(key=key))
649+
else:
650+
result.append((key, value))
651+
return result
652+
629653
def http_request(
630654
self,
631655
verb: str,
@@ -688,6 +712,10 @@ def http_request(
688712
else:
689713
utils.copy_dict(src=kwargs, dest=params)
690714

715+
tuple_params = self._param_dict_to_param_tuples(params=params)
716+
if isinstance(post_data, dict):
717+
post_data = self._prepare_dict_for_api(in_dict=post_data)
718+
691719
opts = self._get_session_opts()
692720

693721
verify = opts.pop("verify")
@@ -710,7 +738,7 @@ def http_request(
710738
url=url,
711739
json=json,
712740
data=data,
713-
params=params,
741+
params=tuple_params,
714742
timeout=timeout,
715743
verify=verify,
716744
stream=streamed,

gitlab/types.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ def set_from_cli(self, cli_value: Any) -> None:
6666
def get_for_api(self) -> Any:
6767
return self._value
6868

69+
def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, Any]]:
70+
return [(key, self._value)]
71+
6972

7073
class _ListArrayAttribute(GitlabAttribute):
7174
"""Helper class to support `list` / `array` types."""
@@ -90,17 +93,31 @@ class ArrayAttribute(_ListArrayAttribute):
9093
"""To support `array` types as documented in
9194
https://docs.gitlab.com/ee/api/#array"""
9295

96+
def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, str]]:
97+
if isinstance(self._value, str):
98+
return [(f"{key}[]", self._value)]
99+
100+
if TYPE_CHECKING:
101+
assert isinstance(self._value, list)
102+
return [(f"{key}[]", str(value)) for value in self._value]
103+
93104

94105
class CommaSeparatedListAttribute(_ListArrayAttribute):
95106
"""For values which are sent to the server as a Comma Separated Values
96107
(CSV) string. We allow them to be specified as a list and we convert it
97108
into a CSV"""
98109

110+
def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, str]]:
111+
return [(key, self.get_for_api())]
112+
99113

100114
class LowercaseStringAttribute(GitlabAttribute):
101115
def get_for_api(self) -> str:
102116
return str(self._value).lower()
103117

118+
def get_as_tuple_list(self, *, key: str) -> List[Tuple[str, str]]:
119+
return [(key, self.get_for_api())]
120+
104121

105122
class FileAttribute(GitlabAttribute):
106123
@staticmethod

gitlab/utils.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,13 @@ def _transform_types(
7777

7878
type_obj = type_cls(data[attr_name])
7979

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)
83-
files[attr_name] = (key, data.pop(attr_name))
80+
# if the type is FileAttribute we need to pass the data as file
81+
if isinstance(type_obj, types.FileAttribute):
82+
if transform_files:
83+
key = type_obj.get_file_name(attr_name)
84+
files[attr_name] = (key, data.pop(attr_name))
8485
else:
85-
data[attr_name] = type_obj.get_for_api()
86+
data[attr_name] = type_obj
8687

8788
return data, files
8889

@@ -94,6 +95,8 @@ def copy_dict(
9495
) -> None:
9596
for k, v in src.items():
9697
if isinstance(v, dict):
98+
# NOTE(jlvillal): This provides some support for the `hash` type
99+
# https://docs.gitlab.com/ee/api/#hash
97100
# Transform dict values to new attributes. For example:
98101
# custom_attributes: {'foo', 'bar'} =>
99102
# "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/test_gitlab_http_methods.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import requests
66
import responses
77

8+
import gitlab
89
from gitlab import GitlabHttpError, GitlabList, GitlabParsingError, RedirectError
910
from gitlab.client import RETRYABLE_TRANSIENT_ERROR_CODES
1011
from tests.unit import helpers
@@ -742,3 +743,20 @@ def test_delete_request_404(gl):
742743
with pytest.raises(GitlabHttpError):
743744
gl.http_delete("/not_there")
744745
assert responses.assert_call_count(url, 1) is True
746+
747+
748+
@responses.activate
749+
def test_array_type_request(gl):
750+
url = "http://localhost/api/v4/projects"
751+
params = "array_var[]=1&array_var[]=2&array_var[]=3"
752+
full_url = f"{url}?array_var%5B%5D=1&array_var%5B%5D=2&array_var%5B%5D=3"
753+
responses.add(
754+
method=responses.GET,
755+
url=url,
756+
json={"name": "project1"},
757+
status=200,
758+
match=[responses.matchers.query_string_matcher(params)],
759+
)
760+
761+
gl.http_get("/projects", array_var=gitlab.types.ArrayAttribute([1, 2, 3]))
762+
assert responses.assert_call_count(full_url, 1) is True

tests/unit/test_types.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def test_gitlab_attribute_get():
7474
o.set_from_cli("whatever2")
7575
assert o.get() == "whatever2"
7676
assert o.get_for_api() == "whatever2"
77+
assert o.get_as_tuple_list(key="foo") == [("foo", "whatever2")]
7778

7879
o = types.GitlabAttribute()
7980
assert o._value is None
@@ -113,6 +114,24 @@ def test_array_attribute_get_for_api_from_int_list():
113114
assert o.get_for_api() == "1,9,7"
114115

115116

117+
def test_array_attribute_get_as_tuple_list_from_list():
118+
o = types.ArrayAttribute(["foo", "bar", "baz"])
119+
assert o.get_as_tuple_list(key="identifier") == [
120+
("identifier[]", "foo"),
121+
("identifier[]", "bar"),
122+
("identifier[]", "baz"),
123+
]
124+
125+
126+
def test_array_attribute_get_as_tuple_list_from_int_list():
127+
o = types.ArrayAttribute([1, 9, 7])
128+
assert o.get_as_tuple_list(key="identifier") == [
129+
("identifier[]", "1"),
130+
("identifier[]", "9"),
131+
("identifier[]", "7"),
132+
]
133+
134+
116135
def test_array_attribute_does_not_split_string():
117136
o = types.ArrayAttribute("foo")
118137
assert o.get_for_api() == "foo"
@@ -135,7 +154,22 @@ def test_csv_string_attribute_get_for_api_from_int_list():
135154
assert o.get_for_api() == "1,9,7"
136155

137156

157+
def test_csv_string_attribute_get_as_tuple_list_from_list():
158+
o = types.CommaSeparatedListAttribute(["foo", "bar", "baz"])
159+
assert o.get_as_tuple_list(key="identifier") == [("identifier", "foo,bar,baz")]
160+
161+
162+
def test_csv_string_attribute_get_as_tuple_list_from_int_list():
163+
o = types.CommaSeparatedListAttribute([1, 9, 7])
164+
assert o.get_as_tuple_list(key="identifier") == [("identifier", "1,9,7")]
165+
166+
138167
# LowercaseStringAttribute tests
139168
def test_lowercase_string_attribute_get_for_api():
140169
o = types.LowercaseStringAttribute("FOO")
141170
assert o.get_for_api() == "foo"
171+
172+
173+
def test_lowercase_string_attribute_get_as_tuple():
174+
o = types.LowercaseStringAttribute("FOO")
175+
assert o.get_as_tuple_list(key="user_name") == [("user_name", "foo")]

0 commit comments

Comments
 (0)