Skip to content

Commit 1940fee

Browse files
author
Gauvain Pocentek
committed
Implement attribute types to handle special cases
Some attributes need to be parsed/modified to work with the API (for instance lists). This patch provides two attribute types that will simplify parts of the code, and fix some CLI bugs. Fixes #443
1 parent 455a8fc commit 1940fee

File tree

6 files changed

+169
-20
lines changed

6 files changed

+169
-20
lines changed

docs/cli.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,12 @@ Use sudo to act as another user (admin only):
235235
236236
$ gitlab project create --name user_project1 --sudo username
237237
238+
List values are comma-separated:
239+
240+
.. code-block:: console
241+
242+
$ gitlab issue list --labels foo,bar
243+
238244
Reading values from files
239245
-------------------------
240246

gitlab/mixins.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,21 @@ def list(self, **kwargs):
108108
GitlabListError: If the server cannot perform the request
109109
"""
110110

111+
# Duplicate data to avoid messing with what the user sent us
112+
data = kwargs.copy()
113+
114+
# We get the attributes that need some special transformation
115+
types = getattr(self, '_types', {})
116+
if types:
117+
for attr_name, type_cls in types.items():
118+
if attr_name in data.keys():
119+
type_obj = type_cls(data[attr_name])
120+
data[attr_name] = type_obj.get_for_api()
121+
111122
# Allow to overwrite the path, handy for custom listings
112-
path = kwargs.pop('path', self.path)
113-
obj = self.gitlab.http_list(path, **kwargs)
123+
path = data.pop('path', self.path)
124+
125+
obj = self.gitlab.http_list(path, **data)
114126
if isinstance(obj, list):
115127
return [self._obj_cls(self, item) for item in obj]
116128
else:
@@ -187,8 +199,22 @@ def create(self, data, **kwargs):
187199
GitlabCreateError: If the server cannot perform the request
188200
"""
189201
self._check_missing_create_attrs(data)
202+
203+
# special handling of the object if needed
190204
if hasattr(self, '_sanitize_data'):
191205
data = self._sanitize_data(data, 'create')
206+
207+
# We get the attributes that need some special transformation
208+
types = getattr(self, '_types', {})
209+
210+
if types:
211+
# Duplicate data to avoid messing with what the user sent us
212+
data = data.copy()
213+
for attr_name, type_cls in types.items():
214+
if attr_name in data.keys():
215+
type_obj = type_cls(data[attr_name])
216+
data[attr_name] = type_obj.get_for_api()
217+
192218
# Handle specific URL for creation
193219
path = kwargs.pop('path', self.path)
194220
server_data = self.gitlab.http_post(path, post_data=data, **kwargs)
@@ -238,11 +264,20 @@ def update(self, id=None, new_data={}, **kwargs):
238264
path = '%s/%s' % (self.path, id)
239265

240266
self._check_missing_update_attrs(new_data)
267+
268+
# special handling of the object if needed
241269
if hasattr(self, '_sanitize_data'):
242270
data = self._sanitize_data(new_data, 'update')
243271
else:
244272
data = new_data
245273

274+
# We get the attributes that need some special transformation
275+
types = getattr(self, '_types', {})
276+
for attr_name, type_cls in types.items():
277+
if attr_name in data.keys():
278+
type_obj = type_cls(data[attr_name])
279+
data[attr_name] = type_obj.get_for_api()
280+
246281
return self.gitlab.http_put(path, post_data=data, **kwargs)
247282

248283

gitlab/tests/test_types.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (C) 2018 Gauvain Pocentek <gauvain@pocentek.net>
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU Lesser General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
18+
try:
19+
import unittest
20+
except ImportError:
21+
import unittest2 as unittest
22+
23+
from gitlab import types
24+
25+
26+
class TestGitlabAttribute(unittest.TestCase):
27+
def test_all(self):
28+
o = types.GitlabAttribute('whatever')
29+
self.assertEqual('whatever', o.get())
30+
31+
o.set_from_cli('whatever2')
32+
self.assertEqual('whatever2', o.get())
33+
34+
self.assertEqual('whatever2', o.get_for_api())
35+
36+
o = types.GitlabAttribute()
37+
self.assertEqual(None, o._value)
38+
39+
40+
class TestListAttribute(unittest.TestCase):
41+
def test_list_input(self):
42+
o = types.ListAttribute()
43+
o.set_from_cli('foo,bar,baz')
44+
self.assertEqual(['foo', 'bar', 'baz'], o.get())
45+
46+
o.set_from_cli('foo')
47+
self.assertEqual(['foo'], o.get())
48+
49+
def test_empty_input(self):
50+
o = types.ListAttribute()
51+
o.set_from_cli('')
52+
self.assertEqual([], o.get())
53+
54+
o.set_from_cli(' ')
55+
self.assertEqual([], o.get())
56+
57+
def test_get_for_api(self):
58+
o = types.ListAttribute()
59+
o.set_from_cli('foo,bar,baz')
60+
self.assertEqual('foo,bar,baz', o.get_for_api())
61+
62+
63+
class TestLowercaseStringAttribute(unittest.TestCase):
64+
def test_get_for_api(self):
65+
o = types.LowercaseStringAttribute('FOO')
66+
self.assertEqual('foo', o.get_for_api())

gitlab/types.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (C) 2018 Gauvain Pocentek <gauvain@pocentek.net>
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU Lesser General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
18+
19+
class GitlabAttribute(object):
20+
def __init__(self, value=None):
21+
self._value = value
22+
23+
def get(self):
24+
return self._value
25+
26+
def set_from_cli(self, cli_value):
27+
self._value = cli_value
28+
29+
def get_for_api(self):
30+
return self._value
31+
32+
33+
class ListAttribute(GitlabAttribute):
34+
def set_from_cli(self, cli_value):
35+
if not cli_value.strip():
36+
self._value = []
37+
else:
38+
self._value = [item.strip() for item in cli_value.split(',')]
39+
40+
def get_for_api(self):
41+
return ",".join(self._value)
42+
43+
44+
class LowercaseStringAttribute(GitlabAttribute):
45+
def get_for_api(self):
46+
return str(self._value).lower()

gitlab/v4/cli.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ def __init__(self, gl, what, action, args):
4545
self.mgr_cls._path = self.mgr_cls._path % self.args
4646
self.mgr = self.mgr_cls(gl)
4747

48+
types = getattr(self.mgr_cls, '_types', {})
49+
if types:
50+
for attr_name, type_cls in types.items():
51+
if attr_name in self.args.keys():
52+
obj = type_cls()
53+
obj.set_from_cli(self.args[attr_name])
54+
self.args[attr_name] = obj.get()
55+
4856
def __call__(self):
4957
method = 'do_%s' % self.action
5058
if hasattr(self, method):

gitlab/v4/objects.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from gitlab import cli
2424
from gitlab.exceptions import * # noqa
2525
from gitlab.mixins import * # noqa
26+
from gitlab import types
2627
from gitlab import utils
2728

2829
VISIBILITY_PRIVATE = 'private'
@@ -315,12 +316,7 @@ class UserManager(CRUDMixin, RESTManager):
315316
'website_url', 'skip_confirmation', 'external', 'organization',
316317
'location')
317318
)
318-
319-
def _sanitize_data(self, data, action):
320-
new_data = data.copy()
321-
if 'confirm' in data:
322-
new_data['confirm'] = str(new_data['confirm']).lower()
323-
return new_data
319+
_types = {'confirm': types.LowercaseStringAttribute}
324320

325321

326322
class CurrentUserEmail(ObjectDeleteMixin, RESTObject):
@@ -528,6 +524,7 @@ class GroupIssueManager(GetFromListMixin, RESTManager):
528524
_obj_cls = GroupIssue
529525
_from_parent_attrs = {'group_id': 'id'}
530526
_list_filters = ('state', 'labels', 'milestone', 'order_by', 'sort')
527+
_types = {'labels': types.ListAttribute}
531528

532529

533530
class GroupMember(SaveMixin, ObjectDeleteMixin, RESTObject):
@@ -736,6 +733,7 @@ class IssueManager(GetFromListMixin, RESTManager):
736733
_path = '/issues'
737734
_obj_cls = Issue
738735
_list_filters = ('state', 'labels', 'order_by', 'sort')
736+
_types = {'labels': types.ListAttribute}
739737

740738

741739
class License(RESTObject):
@@ -1346,12 +1344,7 @@ class ProjectIssueManager(CRUDMixin, RESTManager):
13461344
_update_attrs = (tuple(), ('title', 'description', 'assignee_id',
13471345
'milestone_id', 'labels', 'created_at',
13481346
'updated_at', 'state_event', 'due_date'))
1349-
1350-
def _sanitize_data(self, data, action):
1351-
new_data = data.copy()
1352-
if 'labels' in data:
1353-
new_data['labels'] = ','.join(data['labels'])
1354-
return new_data
1347+
_types = {'labels': types.ListAttribute}
13551348

13561349

13571350
class ProjectMember(SaveMixin, ObjectDeleteMixin, RESTObject):
@@ -1669,12 +1662,7 @@ class ProjectMergeRequestManager(CRUDMixin, RESTManager):
16691662
'description', 'state_event', 'labels',
16701663
'milestone_id'))
16711664
_list_filters = ('iids', 'state', 'order_by', 'sort')
1672-
1673-
def _sanitize_data(self, data, action):
1674-
new_data = data.copy()
1675-
if 'labels' in data:
1676-
new_data['labels'] = ','.join(data['labels'])
1677-
return new_data
1665+
_types = {'labels': types.ListAttribute}
16781666

16791667

16801668
class ProjectMilestone(SaveMixin, ObjectDeleteMixin, RESTObject):

0 commit comments

Comments
 (0)