Skip to content

Commit 3fa330c

Browse files
authored
feat: support mutually exclusive attributes and consolidate validation to fix board lists (#2037)
add exclusive tuple to RequiredOptional data class to support for mutually exclusive attributes consolidate _check_missing_create_attrs and _check_missing_update_attrs from mixins.py into _validate_attrs in utils.py change _create_attrs in board list manager classes from required=('label_ld',) to exclusive=('label_id','asignee_id','milestone_id') closes #1897
1 parent 37eb8e0 commit 3fa330c

File tree

8 files changed

+53
-40
lines changed

8 files changed

+53
-40
lines changed

gitlab/mixins.py

+7-27
Original file line numberDiff line numberDiff line change
@@ -256,15 +256,6 @@ class CreateMixin(_RestManagerBase):
256256
_path: Optional[str]
257257
gitlab: gitlab.Gitlab
258258

259-
def _check_missing_create_attrs(self, data: Dict[str, Any]) -> None:
260-
missing = []
261-
for attr in self._create_attrs.required:
262-
if attr not in data:
263-
missing.append(attr)
264-
continue
265-
if missing:
266-
raise AttributeError(f"Missing attributes: {', '.join(missing)}")
267-
268259
@exc.on_http_error(exc.GitlabCreateError)
269260
def create(
270261
self, data: Optional[Dict[str, Any]] = None, **kwargs: Any
@@ -287,7 +278,7 @@ def create(
287278
if data is None:
288279
data = {}
289280

290-
self._check_missing_create_attrs(data)
281+
utils._validate_attrs(data=data, attributes=self._create_attrs)
291282
data, files = utils._transform_types(data, self._types)
292283

293284
# Handle specific URL for creation
@@ -309,22 +300,6 @@ class UpdateMixin(_RestManagerBase):
309300
_update_uses_post: bool = False
310301
gitlab: gitlab.Gitlab
311302

312-
def _check_missing_update_attrs(self, data: Dict[str, Any]) -> None:
313-
if TYPE_CHECKING:
314-
assert self._obj_cls is not None
315-
# Remove the id field from the required list as it was previously moved
316-
# to the http path.
317-
required = tuple(
318-
[k for k in self._update_attrs.required if k != self._obj_cls._id_attr]
319-
)
320-
missing = []
321-
for attr in required:
322-
if attr not in data:
323-
missing.append(attr)
324-
continue
325-
if missing:
326-
raise AttributeError(f"Missing attributes: {', '.join(missing)}")
327-
328303
def _get_update_method(
329304
self,
330305
) -> Callable[..., Union[Dict[str, Any], requests.Response]]:
@@ -367,7 +342,12 @@ def update(
367342
else:
368343
path = f"{self.path}/{utils.EncodedId(id)}"
369344

370-
self._check_missing_update_attrs(new_data)
345+
excludes = []
346+
if self._obj_cls is not None and self._obj_cls._id_attr is not None:
347+
excludes = [self._obj_cls._id_attr]
348+
utils._validate_attrs(
349+
data=new_data, attributes=self._update_attrs, excludes=excludes
350+
)
371351
new_data, files = utils._transform_types(new_data, self._types)
372352

373353
http_method = self._get_update_method()

gitlab/types.py

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
class RequiredOptional:
2424
required: Tuple[str, ...] = ()
2525
optional: Tuple[str, ...] = ()
26+
exclusive: Tuple[str, ...] = ()
2627

2728

2829
class GitlabAttribute:

gitlab/utils.py

+28-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import traceback
2020
import urllib.parse
2121
import warnings
22-
from typing import Any, Callable, Dict, Optional, Tuple, Type, Union
22+
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
2323

2424
import requests
2525

@@ -161,3 +161,30 @@ def warn(
161161
stacklevel=stacklevel,
162162
source=source,
163163
)
164+
165+
166+
def _validate_attrs(
167+
data: Dict[str, Any],
168+
attributes: types.RequiredOptional,
169+
excludes: Optional[List[str]] = None,
170+
) -> None:
171+
if excludes is None:
172+
excludes = []
173+
174+
if attributes.required:
175+
required = [k for k in attributes.required if k not in excludes]
176+
missing = [attr for attr in required if attr not in data]
177+
if missing:
178+
raise AttributeError(f"Missing attributes: {', '.join(missing)}")
179+
180+
if attributes.exclusive:
181+
exclusives = [attr for attr in data if attr in attributes.exclusive]
182+
if len(exclusives) > 1:
183+
raise AttributeError(
184+
f"Provide only one of these attributes: {', '.join(exclusives)}"
185+
)
186+
if not exclusives:
187+
raise AttributeError(
188+
"Must provide one of these attributes: %(attrs)s"
189+
% {"attrs": ", ".join(attributes.exclusive)}
190+
)

gitlab/v4/objects/boards.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ class GroupBoardListManager(CRUDMixin, RESTManager):
2424
_path = "/groups/{group_id}/boards/{board_id}/lists"
2525
_obj_cls = GroupBoardList
2626
_from_parent_attrs = {"group_id": "group_id", "board_id": "id"}
27-
_create_attrs = RequiredOptional(required=("label_id",))
27+
_create_attrs = RequiredOptional(
28+
exclusive=("label_id", "assignee_id", "milestone_id")
29+
)
2830
_update_attrs = RequiredOptional(required=("position",))
2931

3032
def get(
@@ -55,7 +57,9 @@ class ProjectBoardListManager(CRUDMixin, RESTManager):
5557
_path = "/projects/{project_id}/boards/{board_id}/lists"
5658
_obj_cls = ProjectBoardList
5759
_from_parent_attrs = {"project_id": "project_id", "board_id": "id"}
58-
_create_attrs = RequiredOptional(required=("label_id",))
60+
_create_attrs = RequiredOptional(
61+
exclusive=("label_id", "assignee_id", "milestone_id")
62+
)
5963
_update_attrs = RequiredOptional(required=("position",))
6064

6165
def get(

gitlab/v4/objects/epics.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Any, cast, Dict, Optional, TYPE_CHECKING, Union
22

33
from gitlab import exceptions as exc
4-
from gitlab import types
4+
from gitlab import types, utils
55
from gitlab.base import RESTManager, RESTObject
66
from gitlab.mixins import (
77
CreateMixin,
@@ -107,7 +107,7 @@ def create(
107107
"""
108108
if TYPE_CHECKING:
109109
assert data is not None
110-
CreateMixin._check_missing_create_attrs(self, data)
110+
utils._validate_attrs(data=data, attributes=self._create_attrs)
111111
path = f"{self.path}/{data.pop('issue_id')}"
112112
server_data = self.gitlab.http_post(path, **kwargs)
113113
if TYPE_CHECKING:

gitlab/v4/objects/files.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def create(
145145

146146
if TYPE_CHECKING:
147147
assert data is not None
148-
self._check_missing_create_attrs(data)
148+
utils._validate_attrs(data=data, attributes=self._create_attrs)
149149
new_data = data.copy()
150150
file_path = utils.EncodedId(new_data.pop("file_path"))
151151
path = f"{self.path}/{file_path}"
@@ -179,7 +179,7 @@ def update( # type: ignore
179179
file_path = utils.EncodedId(file_path)
180180
data["file_path"] = file_path
181181
path = f"{self.path}/{file_path}"
182-
self._check_missing_update_attrs(data)
182+
utils._validate_attrs(data=data, attributes=self._update_attrs)
183183
result = self.gitlab.http_put(path, post_data=data, **kwargs)
184184
if TYPE_CHECKING:
185185
assert isinstance(result, dict)

gitlab/v4/objects/issues.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from gitlab import cli
44
from gitlab import exceptions as exc
5-
from gitlab import types
5+
from gitlab import types, utils
66
from gitlab.base import RESTManager, RESTObject
77
from gitlab.mixins import (
88
CreateMixin,
@@ -272,7 +272,7 @@ def create( # type: ignore
272272
GitlabAuthenticationError: If authentication is not correct
273273
GitlabCreateError: If the server cannot perform the request
274274
"""
275-
self._check_missing_create_attrs(data)
275+
utils._validate_attrs(data=data, attributes=self._create_attrs)
276276
if TYPE_CHECKING:
277277
assert self.path is not None
278278
server_data = self.gitlab.http_post(self.path, post_data=data, **kwargs)

tests/unit/mixins/test_mixin_methods.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from gitlab import base
55
from gitlab import types as gl_types
6+
from gitlab import utils
67
from gitlab.mixins import (
78
CreateMixin,
89
DeleteMixin,
@@ -173,11 +174,11 @@ class M(CreateMixin, FakeManager):
173174

174175
mgr = M(gl)
175176
data = {"foo": "bar", "baz": "blah"}
176-
mgr._check_missing_create_attrs(data)
177+
utils._validate_attrs(data=data, attributes=mgr._create_attrs)
177178

178179
data = {"baz": "blah"}
179180
with pytest.raises(AttributeError) as error:
180-
mgr._check_missing_create_attrs(data)
181+
utils._validate_attrs(data=data, attributes=mgr._create_attrs)
181182
assert "foo" in str(error.value)
182183

183184

@@ -239,11 +240,11 @@ class M(UpdateMixin, FakeManager):
239240

240241
mgr = M(gl)
241242
data = {"foo": "bar", "baz": "blah"}
242-
mgr._check_missing_update_attrs(data)
243+
utils._validate_attrs(data=data, attributes=mgr._update_attrs)
243244

244245
data = {"baz": "blah"}
245246
with pytest.raises(AttributeError) as error:
246-
mgr._check_missing_update_attrs(data)
247+
utils._validate_attrs(data=data, attributes=mgr._update_attrs)
247248
assert "foo" in str(error.value)
248249

249250

0 commit comments

Comments
 (0)