summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gitlab/mixins.py15
-rw-r--r--gitlab/types.py22
-rw-r--r--gitlab/utils.py39
-rw-r--r--tests/functional/api/test_groups.py5
-rw-r--r--tests/unit/mixins/test_mixin_methods.py38
-rw-r--r--tests/unit/test_types.py18
-rw-r--r--tests/unit/test_utils.py28
7 files changed, 134 insertions, 31 deletions
diff --git a/gitlab/mixins.py b/gitlab/mixins.py
index f33a1fc..a48c032 100644
--- a/gitlab/mixins.py
+++ b/gitlab/mixins.py
@@ -238,7 +238,12 @@ class ListMixin(HeadMixin, _RestManagerBase):
GitlabListError: If the server cannot perform the request
"""
- data, _ = utils._transform_types(kwargs, self._types, transform_files=False)
+ data, _ = utils._transform_types(
+ data=kwargs,
+ custom_types=self._types,
+ transform_data=True,
+ transform_files=False,
+ )
if self.gitlab.per_page:
data.setdefault("per_page", self.gitlab.per_page)
@@ -303,7 +308,9 @@ class CreateMixin(_RestManagerBase):
data = {}
self._create_attrs.validate_attrs(data=data)
- data, files = utils._transform_types(data, self._types)
+ data, files = utils._transform_types(
+ data=data, custom_types=self._types, transform_data=False
+ )
# Handle specific URL for creation
path = kwargs.pop("path", self.path)
@@ -370,7 +377,9 @@ class UpdateMixin(_RestManagerBase):
if self._obj_cls is not None and self._obj_cls._id_attr is not None:
excludes = [self._obj_cls._id_attr]
self._update_attrs.validate_attrs(data=new_data, excludes=excludes)
- new_data, files = utils._transform_types(new_data, self._types)
+ new_data, files = utils._transform_types(
+ data=new_data, custom_types=self._types, transform_data=False
+ )
http_method = self._get_update_method()
result = http_method(path, post_data=new_data, files=files, **kwargs)
diff --git a/gitlab/types.py b/gitlab/types.py
index f811a6f..d683b70 100644
--- a/gitlab/types.py
+++ b/gitlab/types.py
@@ -63,8 +63,8 @@ class GitlabAttribute:
def set_from_cli(self, cli_value: Any) -> None:
self._value = cli_value
- def get_for_api(self) -> Any:
- return self._value
+ def get_for_api(self, *, key: str) -> Tuple[str, Any]:
+ return (key, self._value)
class _ListArrayAttribute(GitlabAttribute):
@@ -76,20 +76,28 @@ class _ListArrayAttribute(GitlabAttribute):
else:
self._value = [item.strip() for item in cli_value.split(",")]
- def get_for_api(self) -> str:
+ def get_for_api(self, *, key: str) -> Tuple[str, str]:
# Do not comma-split single value passed as string
if isinstance(self._value, str):
- return self._value
+ return (key, self._value)
if TYPE_CHECKING:
assert isinstance(self._value, list)
- return ",".join([str(x) for x in self._value])
+ return (key, ",".join([str(x) for x in self._value]))
class ArrayAttribute(_ListArrayAttribute):
"""To support `array` types as documented in
https://docs.gitlab.com/ee/api/#array"""
+ def get_for_api(self, *, key: str) -> Tuple[str, Any]:
+ if isinstance(self._value, str):
+ return (f"{key}[]", self._value)
+
+ if TYPE_CHECKING:
+ assert isinstance(self._value, list)
+ return (f"{key}[]", self._value)
+
class CommaSeparatedListAttribute(_ListArrayAttribute):
"""For values which are sent to the server as a Comma Separated Values
@@ -98,8 +106,8 @@ class CommaSeparatedListAttribute(_ListArrayAttribute):
class LowercaseStringAttribute(GitlabAttribute):
- def get_for_api(self) -> str:
- return str(self._value).lower()
+ def get_for_api(self, *, key: str) -> Tuple[str, str]:
+ return (key, str(self._value).lower())
class FileAttribute(GitlabAttribute):
diff --git a/gitlab/utils.py b/gitlab/utils.py
index 4d2ec8d..f3d97f7 100644
--- a/gitlab/utils.py
+++ b/gitlab/utils.py
@@ -55,34 +55,53 @@ def response_content(
def _transform_types(
- data: Dict[str, Any], custom_types: dict, *, transform_files: Optional[bool] = True
+ data: Dict[str, Any],
+ custom_types: dict,
+ *,
+ transform_data: bool,
+ transform_files: Optional[bool] = True,
) -> Tuple[dict, dict]:
"""Copy the data dict with attributes that have custom types and transform them
before being sent to the server.
- If ``transform_files`` is ``True`` (default), also populates the ``files`` dict for
+ ``transform_files``: If ``True`` (default), also populates the ``files`` dict for
FileAttribute types with tuples to prepare fields for requests' MultipartEncoder:
https://toolbelt.readthedocs.io/en/latest/user.html#multipart-form-data-encoder
+ ``transform_data``: If ``True`` transforms the ``data`` dict with fields
+ suitable for encoding as query parameters for GitLab's API:
+ https://docs.gitlab.com/ee/api/#encoding-api-parameters-of-array-and-hash-types
+
Returns:
A tuple of the transformed data dict and files dict"""
# Duplicate data to avoid messing with what the user sent us
data = data.copy()
+ if not transform_files and not transform_data:
+ return data, {}
+
files = {}
- for attr_name, type_cls in custom_types.items():
+ for attr_name, attr_class in custom_types.items():
if attr_name not in data:
continue
- type_obj = type_cls(data[attr_name])
+ gitlab_attribute = attr_class(data[attr_name])
- # if the type if FileAttribute we need to pass the data as file
- if transform_files and isinstance(type_obj, types.FileAttribute):
- key = type_obj.get_file_name(attr_name)
+ # if the type is FileAttribute we need to pass the data as file
+ if isinstance(gitlab_attribute, types.FileAttribute) and transform_files:
+ key = gitlab_attribute.get_file_name(attr_name)
files[attr_name] = (key, data.pop(attr_name))
- else:
- data[attr_name] = type_obj.get_for_api()
+ continue
+
+ if not transform_data:
+ continue
+
+ if isinstance(gitlab_attribute, types.GitlabAttribute):
+ key, value = gitlab_attribute.get_for_api(key=attr_name)
+ if key != attr_name:
+ del data[attr_name]
+ data[key] = value
return data, files
@@ -94,6 +113,8 @@ def copy_dict(
) -> None:
for k, v in src.items():
if isinstance(v, dict):
+ # NOTE(jlvillal): This provides some support for the `hash` type
+ # https://docs.gitlab.com/ee/api/#hash
# Transform dict values to new attributes. For example:
# custom_attributes: {'foo', 'bar'} =>
# "custom_attributes['foo']": "bar"
diff --git a/tests/functional/api/test_groups.py b/tests/functional/api/test_groups.py
index 88e5a36..7688273 100644
--- a/tests/functional/api/test_groups.py
+++ b/tests/functional/api/test_groups.py
@@ -99,6 +99,11 @@ def test_groups(gl):
assert len(group1.members.list()) == 3
assert len(group2.members.list()) == 2
+ # Test `user_ids` array
+ result = group1.members.list(user_ids=[user.id, 99999])
+ assert len(result) == 1
+ assert result[0].id == user.id
+
group1.members.delete(user.id)
assert user not in group1.members.list()
assert group1.members_all.list()
diff --git a/tests/unit/mixins/test_mixin_methods.py b/tests/unit/mixins/test_mixin_methods.py
index 68b59a2..004d190 100644
--- a/tests/unit/mixins/test_mixin_methods.py
+++ b/tests/unit/mixins/test_mixin_methods.py
@@ -206,6 +206,25 @@ def test_list_mixin(gl):
@responses.activate
+def test_list_mixin_with_attributes(gl):
+ class M(ListMixin, FakeManager):
+ _types = {"my_array": gl_types.ArrayAttribute}
+
+ url = "http://localhost/api/v4/tests"
+ responses.add(
+ method=responses.GET,
+ headers={},
+ url=url,
+ json=[],
+ status=200,
+ match=[responses.matchers.query_param_matcher({"my_array[]": ["1", "2", "3"]})],
+ )
+
+ mgr = M(gl)
+ mgr.list(iterator=True, my_array=[1, 2, 3])
+
+
+@responses.activate
def test_list_other_url(gl):
class M(ListMixin, FakeManager):
pass
@@ -295,6 +314,25 @@ def test_create_mixin_custom_path(gl):
assert responses.assert_call_count(url, 1) is True
+@responses.activate
+def test_create_mixin_with_attributes(gl):
+ class M(CreateMixin, FakeManager):
+ _types = {"my_array": gl_types.ArrayAttribute}
+
+ url = "http://localhost/api/v4/tests"
+ responses.add(
+ method=responses.POST,
+ headers={},
+ url=url,
+ json={},
+ status=200,
+ match=[responses.matchers.json_params_matcher({"my_array": [1, 2, 3]})],
+ )
+
+ mgr = M(gl)
+ mgr.create({"my_array": [1, 2, 3]})
+
+
def test_update_mixin_missing_attrs(gl):
class M(UpdateMixin, FakeManager):
_update_attrs = gl_types.RequiredOptional(
diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py
index c06e9d0..4f18c51 100644
--- a/tests/unit/test_types.py
+++ b/tests/unit/test_types.py
@@ -73,7 +73,7 @@ def test_gitlab_attribute_get():
o.set_from_cli("whatever2")
assert o.get() == "whatever2"
- assert o.get_for_api() == "whatever2"
+ assert o.get_for_api(key="spam") == ("spam", "whatever2")
o = types.GitlabAttribute()
assert o._value is None
@@ -100,42 +100,42 @@ def test_array_attribute_empty_input():
def test_array_attribute_get_for_api_from_cli():
o = types.ArrayAttribute()
o.set_from_cli("foo,bar,baz")
- assert o.get_for_api() == "foo,bar,baz"
+ assert o.get_for_api(key="spam") == ("spam[]", ["foo", "bar", "baz"])
def test_array_attribute_get_for_api_from_list():
o = types.ArrayAttribute(["foo", "bar", "baz"])
- assert o.get_for_api() == "foo,bar,baz"
+ assert o.get_for_api(key="spam") == ("spam[]", ["foo", "bar", "baz"])
def test_array_attribute_get_for_api_from_int_list():
o = types.ArrayAttribute([1, 9, 7])
- assert o.get_for_api() == "1,9,7"
+ assert o.get_for_api(key="spam") == ("spam[]", [1, 9, 7])
def test_array_attribute_does_not_split_string():
o = types.ArrayAttribute("foo")
- assert o.get_for_api() == "foo"
+ assert o.get_for_api(key="spam") == ("spam[]", "foo")
# CommaSeparatedListAttribute tests
def test_csv_string_attribute_get_for_api_from_cli():
o = types.CommaSeparatedListAttribute()
o.set_from_cli("foo,bar,baz")
- assert o.get_for_api() == "foo,bar,baz"
+ assert o.get_for_api(key="spam") == ("spam", "foo,bar,baz")
def test_csv_string_attribute_get_for_api_from_list():
o = types.CommaSeparatedListAttribute(["foo", "bar", "baz"])
- assert o.get_for_api() == "foo,bar,baz"
+ assert o.get_for_api(key="spam") == ("spam", "foo,bar,baz")
def test_csv_string_attribute_get_for_api_from_int_list():
o = types.CommaSeparatedListAttribute([1, 9, 7])
- assert o.get_for_api() == "1,9,7"
+ assert o.get_for_api(key="spam") == ("spam", "1,9,7")
# LowercaseStringAttribute tests
def test_lowercase_string_attribute_get_for_api():
o = types.LowercaseStringAttribute("FOO")
- assert o.get_for_api() == "foo"
+ assert o.get_for_api(key="spam") == ("spam", "foo")
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index ce2e776..6038d84 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -155,7 +155,7 @@ def test_remove_none_from_dict(dictionary, expected):
def test_transform_types_copies_data_with_empty_files():
data = {"attr": "spam"}
- new_data, files = utils._transform_types(data, {})
+ new_data, files = utils._transform_types(data, {}, transform_data=True)
assert new_data is not data
assert new_data == data
@@ -165,7 +165,7 @@ def test_transform_types_copies_data_with_empty_files():
def test_transform_types_with_transform_files_populates_files():
custom_types = {"attr": types.FileAttribute}
data = {"attr": "spam"}
- new_data, files = utils._transform_types(data, custom_types)
+ new_data, files = utils._transform_types(data, custom_types, transform_data=True)
assert new_data == {}
assert files["attr"] == ("attr", "spam")
@@ -174,7 +174,29 @@ def test_transform_types_with_transform_files_populates_files():
def test_transform_types_without_transform_files_populates_data_with_empty_files():
custom_types = {"attr": types.FileAttribute}
data = {"attr": "spam"}
- new_data, files = utils._transform_types(data, custom_types, transform_files=False)
+ new_data, files = utils._transform_types(
+ data, custom_types, transform_files=False, transform_data=True
+ )
assert new_data == {"attr": "spam"}
assert files == {}
+
+
+def test_transform_types_params_array():
+ data = {"attr": [1, 2, 3]}
+ custom_types = {"attr": types.ArrayAttribute}
+ new_data, files = utils._transform_types(data, custom_types, transform_data=True)
+
+ assert new_data is not data
+ assert new_data == {"attr[]": [1, 2, 3]}
+ assert files == {}
+
+
+def test_transform_types_not_params_array():
+ data = {"attr": [1, 2, 3]}
+ custom_types = {"attr": types.ArrayAttribute}
+ new_data, files = utils._transform_types(data, custom_types, transform_data=False)
+
+ assert new_data is not data
+ assert new_data == data
+ assert files == {}