Skip to content

feat: add support for mutually exclusive attributes, consolidate attribute validation, fix boards.py _create_attr #2037

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 7 additions & 27 deletions gitlab/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,15 +256,6 @@ class CreateMixin(_RestManagerBase):
_path: Optional[str]
gitlab: gitlab.Gitlab

def _check_missing_create_attrs(self, data: Dict[str, Any]) -> None:
missing = []
for attr in self._create_attrs.required:
if attr not in data:
missing.append(attr)
continue
if missing:
raise AttributeError(f"Missing attributes: {', '.join(missing)}")

@exc.on_http_error(exc.GitlabCreateError)
def create(
self, data: Optional[Dict[str, Any]] = None, **kwargs: Any
Expand All @@ -287,7 +278,7 @@ def create(
if data is None:
data = {}

self._check_missing_create_attrs(data)
utils._validate_attrs(data=data, attributes=self._create_attrs)
data, files = utils._transform_types(data, self._types)

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

def _check_missing_update_attrs(self, data: Dict[str, Any]) -> None:
if TYPE_CHECKING:
assert self._obj_cls is not None
# Remove the id field from the required list as it was previously moved
# to the http path.
required = tuple(
[k for k in self._update_attrs.required if k != self._obj_cls._id_attr]
)
missing = []
for attr in required:
if attr not in data:
missing.append(attr)
continue
if missing:
raise AttributeError(f"Missing attributes: {', '.join(missing)}")

def _get_update_method(
self,
) -> Callable[..., Union[Dict[str, Any], requests.Response]]:
Expand Down Expand Up @@ -367,7 +342,12 @@ def update(
else:
path = f"{self.path}/{utils.EncodedId(id)}"

self._check_missing_update_attrs(new_data)
excludes = []
if self._obj_cls is not None and self._obj_cls._id_attr is not None:
excludes = [self._obj_cls._id_attr]
utils._validate_attrs(
data=new_data, attributes=self._update_attrs, excludes=excludes
)
new_data, files = utils._transform_types(new_data, self._types)

http_method = self._get_update_method()
Expand Down
1 change: 1 addition & 0 deletions gitlab/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
class RequiredOptional:
required: Tuple[str, ...] = ()
optional: Tuple[str, ...] = ()
exclusive: Tuple[str, ...] = ()


class GitlabAttribute:
Expand Down
29 changes: 28 additions & 1 deletion gitlab/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import traceback
import urllib.parse
import warnings
from typing import Any, Callable, Dict, Optional, Tuple, Type, Union
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union

import requests

Expand Down Expand Up @@ -161,3 +161,30 @@ def warn(
stacklevel=stacklevel,
source=source,
)


def _validate_attrs(
data: Dict[str, Any],
attributes: types.RequiredOptional,
excludes: Optional[List[str]] = None,
) -> None:
if excludes is None:
excludes = []

if attributes.required:
required = [k for k in attributes.required if k not in excludes]
missing = [attr for attr in required if attr not in data]
if missing:
raise AttributeError(f"Missing attributes: {', '.join(missing)}")

if attributes.exclusive:
exclusives = [attr for attr in data if attr in attributes.exclusive]
if len(exclusives) > 1:
raise AttributeError(
f"Provide only one of these attributes: {', '.join(exclusives)}"
)
if not exclusives:
raise AttributeError(
"Must provide one of these attributes: %(attrs)s"
% {"attrs": ", ".join(attributes.exclusive)}
)
8 changes: 6 additions & 2 deletions gitlab/v4/objects/boards.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ class GroupBoardListManager(CRUDMixin, RESTManager):
_path = "/groups/{group_id}/boards/{board_id}/lists"
_obj_cls = GroupBoardList
_from_parent_attrs = {"group_id": "group_id", "board_id": "id"}
_create_attrs = RequiredOptional(required=("label_id",))
_create_attrs = RequiredOptional(
exclusive=("label_id", "assignee_id", "milestone_id")
)
_update_attrs = RequiredOptional(required=("position",))

def get(
Expand Down Expand Up @@ -55,7 +57,9 @@ class ProjectBoardListManager(CRUDMixin, RESTManager):
_path = "/projects/{project_id}/boards/{board_id}/lists"
_obj_cls = ProjectBoardList
_from_parent_attrs = {"project_id": "project_id", "board_id": "id"}
_create_attrs = RequiredOptional(required=("label_id",))
_create_attrs = RequiredOptional(
exclusive=("label_id", "assignee_id", "milestone_id")
)
_update_attrs = RequiredOptional(required=("position",))

def get(
Expand Down
4 changes: 2 additions & 2 deletions gitlab/v4/objects/epics.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Any, cast, Dict, Optional, TYPE_CHECKING, Union

from gitlab import exceptions as exc
from gitlab import types
from gitlab import types, utils
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import (
CreateMixin,
Expand Down Expand Up @@ -107,7 +107,7 @@ def create(
"""
if TYPE_CHECKING:
assert data is not None
CreateMixin._check_missing_create_attrs(self, data)
utils._validate_attrs(data=data, attributes=self._create_attrs)
path = f"{self.path}/{data.pop('issue_id')}"
server_data = self.gitlab.http_post(path, **kwargs)
if TYPE_CHECKING:
Expand Down
4 changes: 2 additions & 2 deletions gitlab/v4/objects/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def create(

if TYPE_CHECKING:
assert data is not None
self._check_missing_create_attrs(data)
utils._validate_attrs(data=data, attributes=self._create_attrs)
new_data = data.copy()
file_path = utils.EncodedId(new_data.pop("file_path"))
path = f"{self.path}/{file_path}"
Expand Down Expand Up @@ -179,7 +179,7 @@ def update( # type: ignore
file_path = utils.EncodedId(file_path)
data["file_path"] = file_path
path = f"{self.path}/{file_path}"
self._check_missing_update_attrs(data)
utils._validate_attrs(data=data, attributes=self._update_attrs)
result = self.gitlab.http_put(path, post_data=data, **kwargs)
if TYPE_CHECKING:
assert isinstance(result, dict)
Expand Down
4 changes: 2 additions & 2 deletions gitlab/v4/objects/issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from gitlab import cli
from gitlab import exceptions as exc
from gitlab import types
from gitlab import types, utils
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import (
CreateMixin,
Expand Down Expand Up @@ -272,7 +272,7 @@ def create( # type: ignore
GitlabAuthenticationError: If authentication is not correct
GitlabCreateError: If the server cannot perform the request
"""
self._check_missing_create_attrs(data)
utils._validate_attrs(data=data, attributes=self._create_attrs)
if TYPE_CHECKING:
assert self.path is not None
server_data = self.gitlab.http_post(self.path, post_data=data, **kwargs)
Expand Down
9 changes: 5 additions & 4 deletions tests/unit/mixins/test_mixin_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from gitlab import base
from gitlab import types as gl_types
from gitlab import utils
from gitlab.mixins import (
CreateMixin,
DeleteMixin,
Expand Down Expand Up @@ -173,11 +174,11 @@ class M(CreateMixin, FakeManager):

mgr = M(gl)
data = {"foo": "bar", "baz": "blah"}
mgr._check_missing_create_attrs(data)
utils._validate_attrs(data=data, attributes=mgr._create_attrs)

data = {"baz": "blah"}
with pytest.raises(AttributeError) as error:
mgr._check_missing_create_attrs(data)
utils._validate_attrs(data=data, attributes=mgr._create_attrs)
assert "foo" in str(error.value)


Expand Down Expand Up @@ -239,11 +240,11 @@ class M(UpdateMixin, FakeManager):

mgr = M(gl)
data = {"foo": "bar", "baz": "blah"}
mgr._check_missing_update_attrs(data)
utils._validate_attrs(data=data, attributes=mgr._update_attrs)

data = {"baz": "blah"}
with pytest.raises(AttributeError) as error:
mgr._check_missing_update_attrs(data)
utils._validate_attrs(data=data, attributes=mgr._update_attrs)
assert "foo" in str(error.value)


Expand Down