Skip to content

Commit 1b70580

Browse files
nejchJohnVillalovos
authored andcommitted
feat(objects): add support for descendant groups API
1 parent 237b97c commit 1b70580

File tree

4 files changed

+101
-0
lines changed

4 files changed

+101
-0
lines changed

docs/gl_objects/groups.rst

+25
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,31 @@ List the subgroups for a group::
167167
real_group = gl.groups.get(subgroup_id, lazy=True)
168168
real_group.issues.list()
169169

170+
Descendant Groups
171+
=================
172+
173+
Reference
174+
---------
175+
176+
* v4 API:
177+
178+
+ :class:`gitlab.v4.objects.GroupDescendantGroup`
179+
+ :class:`gitlab.v4.objects.GroupDescendantGroupManager`
180+
+ :attr:`gitlab.v4.objects.Group.descendant_groups`
181+
182+
Examples
183+
--------
184+
185+
List the descendant groups of a group::
186+
187+
descendant_groups = group.descendant_groups.list()
188+
189+
.. note::
190+
191+
Like the ``GroupSubgroup`` objects described above, ``GroupDescendantGroup``
192+
objects do not expose the same API as the ``Group`` objects. Create a new
193+
``Group`` object instead if needed, as shown in the subgroup example.
194+
170195
Group custom attributes
171196
=======================
172197

gitlab/v4/objects/groups.py

+17
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
__all__ = [
3232
"Group",
3333
"GroupManager",
34+
"GroupDescendantGroup",
35+
"GroupDescendantGroupManager",
3436
"GroupSubgroup",
3537
"GroupSubgroupManager",
3638
]
@@ -45,6 +47,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
4547
("billable_members", "GroupBillableMemberManager"),
4648
("boards", "GroupBoardManager"),
4749
("customattributes", "GroupCustomAttributeManager"),
50+
("descendant_groups", "GroupDescendantGroupManager"),
4851
("exports", "GroupExportManager"),
4952
("epics", "GroupEpicManager"),
5053
("imports", "GroupImportManager"),
@@ -310,3 +313,17 @@ class GroupSubgroupManager(ListMixin, RESTManager):
310313
"min_access_level",
311314
)
312315
_types = {"skip_groups": types.ListAttribute}
316+
317+
318+
class GroupDescendantGroup(RESTObject):
319+
pass
320+
321+
322+
class GroupDescendantGroupManager(GroupSubgroupManager):
323+
"""
324+
This manager inherits from GroupSubgroupManager as descendant groups
325+
share all attributes with subgroups, except the path and object class.
326+
"""
327+
328+
_path = "/groups/%(group_id)s/descendant_groups"
329+
_obj_cls = GroupDescendantGroup

tests/functional/api/test_groups.py

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def test_groups(gl):
3232
assert len(gl.groups.list(search="oup1")) == 1
3333
assert group3.parent_id == p_id
3434
assert group2.subgroups.list()[0].id == group3.id
35+
assert group2.descendant_groups.list()[0].id == group3.id
3536

3637
filtered_groups = gl.groups.list(skip_groups=[group3.id, group4.id])
3738
assert group3 not in filtered_groups

tests/unit/objects/test_groups.py

+58
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,41 @@
22
GitLab API: https://docs.gitlab.com/ce/api/groups.html
33
"""
44

5+
import re
6+
57
import pytest
68
import responses
79

810
import gitlab
11+
from gitlab.v4.objects import GroupDescendantGroup, GroupSubgroup
12+
13+
subgroup_descgroup_content = [
14+
{
15+
"id": 2,
16+
"name": "Bar Group",
17+
"path": "foo/bar",
18+
"description": "A subgroup of Foo Group",
19+
"visibility": "public",
20+
"share_with_group_lock": False,
21+
"require_two_factor_authentication": False,
22+
"two_factor_grace_period": 48,
23+
"project_creation_level": "developer",
24+
"auto_devops_enabled": None,
25+
"subgroup_creation_level": "owner",
26+
"emails_disabled": None,
27+
"mentions_disabled": None,
28+
"lfs_enabled": True,
29+
"default_branch_protection": 2,
30+
"avatar_url": "http://gitlab.example.com/uploads/group/avatar/1/bar.jpg",
31+
"web_url": "http://gitlab.example.com/groups/foo/bar",
32+
"request_access_enabled": False,
33+
"full_name": "Bar Group",
34+
"full_path": "foo/bar",
35+
"file_template_project_id": 1,
36+
"parent_id": 123,
37+
"created_at": "2020-01-15T12:36:29.590Z",
38+
},
39+
]
940

1041

1142
@pytest.fixture
@@ -37,6 +68,21 @@ def resp_groups():
3768
yield rsps
3869

3970

71+
@pytest.fixture
72+
def resp_list_subgroups_descendant_groups():
73+
with responses.RequestsMock() as rsps:
74+
rsps.add(
75+
method=responses.GET,
76+
url=re.compile(
77+
r"http://localhost/api/v4/groups/1/(subgroups|descendant_groups)"
78+
),
79+
json=subgroup_descgroup_content,
80+
content_type="application/json",
81+
status=200,
82+
)
83+
yield rsps
84+
85+
4086
@pytest.fixture
4187
def resp_create_import(accepted_content):
4288
with responses.RequestsMock() as rsps:
@@ -71,6 +117,18 @@ def test_create_group_export(group, resp_export):
71117
assert export.message == "202 Accepted"
72118

73119

120+
def test_list_group_subgroups(group, resp_list_subgroups_descendant_groups):
121+
subgroups = group.subgroups.list()
122+
assert isinstance(subgroups[0], GroupSubgroup)
123+
assert subgroups[0].path == subgroup_descgroup_content[0]["path"]
124+
125+
126+
def test_list_group_descendant_groups(group, resp_list_subgroups_descendant_groups):
127+
descendant_groups = group.descendant_groups.list()
128+
assert isinstance(descendant_groups[0], GroupDescendantGroup)
129+
assert descendant_groups[0].path == subgroup_descgroup_content[0]["path"]
130+
131+
74132
@pytest.mark.skip("GitLab API endpoint not implemented")
75133
def test_refresh_group_export_status(group, resp_export):
76134
export = group.exports.create()

0 commit comments

Comments
 (0)