Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit3e855f3

Browse files
fix(epics): use actual group_id for save/delete operations on nested epics
When an epic belonging to a subgroup is retrieved through a parentgroup's epic listing, save() and delete() operations would fail becausethey used the parent group's path instead of the epic's actual group_id.This commit overrides save() and delete() methods in GroupEpic to usethe epic's group_id attribute to construct the correct API path, ensuringoperations work correctly regardless of how the epic was retrieved.Also add the ability to pass a custom path using `_pg_custom_path` tothe `UpdateMixin.update()` and `SaveMixin.save()` methods. This allowedthe override of the `update()` method to re-use the `SaveMixin.save()`method.Closes:#3261
1 parent0f5655c commit3e855f3

File tree

4 files changed

+162
-4
lines changed

4 files changed

+162
-4
lines changed

‎gitlab/mixins.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ def update(
292292
self,
293293
id:str|int|None=None,
294294
new_data:dict[str,Any]|None=None,
295+
_pg_custom_path:str|None=None,
295296
**kwargs:Any,
296297
)->dict[str,Any]:
297298
"""Update an object on the server.
@@ -314,6 +315,8 @@ def update(
314315
path=self.path
315316
else:
316317
path=f"{self.path}/{utils.EncodedId(id)}"
318+
if_pg_custom_pathisnotNone:
319+
path=_pg_custom_path
317320

318321
excludes= []
319322
ifself._obj_clsisnotNoneandself._obj_cls._id_attrisnotNone:
@@ -413,7 +416,9 @@ def _get_updated_data(self) -> dict[str, Any]:
413416

414417
returnupdated_data
415418

416-
defsave(self,**kwargs:Any)->dict[str,Any]|None:
419+
defsave(
420+
self,_pg_custom_path:str|None=None,**kwargs:Any
421+
)->dict[str,Any]|None:
417422
"""Save the changes made to the object to the server.
418423
419424
The object is updated to match what the server returns.
@@ -437,7 +442,9 @@ def save(self, **kwargs: Any) -> dict[str, Any] | None:
437442
obj_id=self.encoded_id
438443
ifTYPE_CHECKING:
439444
assertisinstance(self.manager,UpdateMixin)
440-
server_data=self.manager.update(obj_id,updated_data,**kwargs)
445+
server_data=self.manager.update(
446+
id=obj_id,new_data=updated_data,_pg_custom_path=_pg_custom_path,**kwargs
447+
)
441448
self._update_attrs(server_data)
442449
returnserver_data
443450

‎gitlab/v4/objects/epics.py‎

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
fromtypingimportAny,TYPE_CHECKING
44

5+
importgitlab.utils
56
fromgitlabimportexceptionsasexc
67
fromgitlabimporttypes
78
fromgitlab.baseimportRESTObject
@@ -24,11 +25,75 @@
2425

2526
classGroupEpic(ObjectDeleteMixin,SaveMixin,RESTObject):
2627
_id_attr="iid"
28+
manager:GroupEpicManager
2729

2830
issues:GroupEpicIssueManager
2931
resourcelabelevents:GroupEpicResourceLabelEventManager
3032
notes:GroupEpicNoteManager
3133

34+
def_epic_path(self)->str:
35+
"""Return the API path for this epic using its real group."""
36+
ifnothasattr(self,"group_id")orself.group_idisNone:
37+
raiseAttributeError(
38+
"Cannot compute epic path: attribute 'group_id' is missing."
39+
)
40+
encoded_group_id=gitlab.utils.EncodedId(self.group_id)
41+
returnf"/groups/{encoded_group_id}/epics/{self.encoded_id}"
42+
43+
@exc.on_http_error(exc.GitlabUpdateError)
44+
defsave(
45+
self,_pg_custom_path:str|None=None,**kwargs:Any
46+
)->dict[str,Any]|None:
47+
"""Save the changes made to the object to the server.
48+
49+
The object is updated to match what the server returns.
50+
51+
This method uses the epic's group_id attribute to construct the correct
52+
API path. This is important when the epic was retrieved from a parent
53+
group but actually belongs to a sub-group.
54+
55+
Args:
56+
**kwargs: Extra options to send to the server (e.g. sudo)
57+
58+
Returns:
59+
The new object data (*not* a RESTObject)
60+
61+
Raises:
62+
GitlabAuthenticationError: If authentication is not correct
63+
GitlabUpdateError: If the server cannot perform the request
64+
"""
65+
if_pg_custom_pathisnotNone:
66+
raiseValueError(
67+
f"`_pg_custom_path` is not allowed to be set:{_pg_custom_path=}"
68+
)
69+
# Use the epic's actual group_id to construct the correct path.
70+
path=self._epic_path()
71+
72+
# Call SaveMixin.save() method
73+
returnsuper().save(_pg_custom_path=path,**kwargs)
74+
75+
@exc.on_http_error(exc.GitlabDeleteError)
76+
defdelete(self,**kwargs:Any)->None:
77+
"""Delete the object from the server.
78+
79+
This method uses the epic's group_id attribute to construct the correct
80+
API path. This is important when the epic was retrieved from a parent
81+
group but actually belongs to a sub-group.
82+
83+
Args:
84+
**kwargs: Extra options to send to the server (e.g. sudo)
85+
86+
Raises:
87+
GitlabAuthenticationError: If authentication is not correct
88+
GitlabDeleteError: If the server cannot perform the request
89+
"""
90+
ifTYPE_CHECKING:
91+
assertself.encoded_idisnotNone
92+
93+
# Use the epic's actual group_id to construct the correct path.
94+
path=self._epic_path()
95+
self.manager.gitlab.http_delete(path,**kwargs)
96+
3297

3398
classGroupEpicManager(CRUDMixin[GroupEpic]):
3499
_path="/groups/{group_id}/epics"
@@ -51,7 +116,7 @@ class GroupEpicIssue(ObjectDeleteMixin, SaveMixin, RESTObject):
51116
# 'self.manager.update()' call in the 'save' method.
52117
manager:GroupEpicIssueManager
53118

54-
defsave(self,**kwargs:Any)->None:
119+
defsave(self,_pg_custom_path:str|None=None,**kwargs:Any)->None:
55120
"""Save the changes made to the object to the server.
56121
57122
The object is updated to match what the server returns.
@@ -70,7 +135,9 @@ def save(self, **kwargs: Any) -> None:
70135

71136
# call the manager
72137
obj_id=self.encoded_id
73-
self.manager.update(obj_id,updated_data,**kwargs)
138+
self.manager.update(
139+
id=obj_id,data=updated_data,_pg_custom_path=_pg_custom_path,**kwargs
140+
)
74141

75142

76143
classGroupEpicIssueManager(

‎tests/functional/api/test_epics.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
importuuid
2+
13
importpytest
24

5+
fromtests.functionalimporthelpers
6+
37
pytestmark=pytest.mark.gitlab_premium
48

59

@@ -30,3 +34,39 @@ def test_epic_notes(epic):
3034

3135
epic.notes.create({"body":"Test note"})
3236
assertepic.notes.list()
37+
38+
39+
deftest_epic_save_from_parent_group_updates_subgroup_epic(gl,group):
40+
subgroup_id=uuid.uuid4().hex
41+
subgroup=gl.groups.create(
42+
{
43+
"name":f"subgroup-{subgroup_id}",
44+
"path":f"sg-{subgroup_id}",
45+
"parent_id":group.id,
46+
}
47+
)
48+
49+
nested_epic=subgroup.epics.create(
50+
{"title":f"Nested epic{subgroup_id}","description":"Nested epic"}
51+
)
52+
53+
try:
54+
fetched_epics=group.epics.list(search=nested_epic.title)
55+
assertfetched_epics,"Expected to discover nested epic via parent group list"
56+
57+
fetched_epic=next(
58+
(epicforepicinfetched_epicsifepic.id==nested_epic.id),None
59+
)
60+
assert (
61+
fetched_epicisnotNone
62+
),"Parent group listing did not include nested epic"
63+
64+
new_label=f"nested-{subgroup_id}"
65+
fetched_epic.labels= [new_label]
66+
fetched_epic.save()
67+
68+
refreshed_epic=subgroup.epics.get(nested_epic.iid)
69+
assertnew_labelinrefreshed_epic.labels
70+
finally:
71+
helpers.safe_delete(nested_epic)
72+
helpers.safe_delete(subgroup)

‎tests/unit/objects/test_epics.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
importresponses
2+
3+
fromgitlab.v4.objects.epicsimportGroupEpic
4+
5+
6+
def_build_epic(manager,iid=3,group_id=2,title="Epic"):
7+
data= {"iid":iid,"group_id":group_id,"title":title}
8+
returnGroupEpic(manager,data)
9+
10+
11+
deftest_group_epic_save_uses_actual_group_path(group):
12+
epic_manager=group.epics
13+
epic=_build_epic(epic_manager,title="Original")
14+
epic.title="Updated"
15+
16+
withresponses.RequestsMock()asrsps:
17+
rsps.add(
18+
method=responses.PUT,
19+
url="http://localhost/api/v4/groups/2/epics/3",
20+
json={"iid":3,"group_id":2,"title":"Updated"},
21+
content_type="application/json",
22+
status=200,
23+
match=[responses.matchers.json_params_matcher({"title":"Updated"})],
24+
)
25+
26+
epic.save()
27+
28+
assertepic.title=="Updated"
29+
30+
31+
deftest_group_epic_delete_uses_actual_group_path(group):
32+
epic_manager=group.epics
33+
epic=_build_epic(epic_manager)
34+
35+
withresponses.RequestsMock()asrsps:
36+
rsps.add(
37+
method=responses.DELETE,
38+
url="http://localhost/api/v4/groups/2/epics/3",
39+
status=204,
40+
)
41+
42+
epic.delete()
43+
44+
assertlen(epic._updated_attrs)==0

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp