From e3d50b5e768fd398eee4a099125b1f87618f7428 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 25 Jun 2017 08:37:52 +0200 Subject: [PATCH] Refactor the CLI v3 and v4 CLI will be very different, so start moving things in their own folders. For now v4 isn't working at all. --- gitlab/cli.py | 105 ++++++++++++++++ gitlab/tests/test_cli.py | 37 +++--- gitlab/v3/cli.py | 257 +++++++++++++++------------------------ 3 files changed, 222 insertions(+), 177 deletions(-) create mode 100644 gitlab/cli.py diff --git a/gitlab/cli.py b/gitlab/cli.py new file mode 100644 index 000000000..f23fff9d3 --- /dev/null +++ b/gitlab/cli.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013-2017 Gauvain Pocentek +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +from __future__ import print_function +from __future__ import absolute_import +import argparse +import importlib +import re +import sys + +import gitlab.config + +camel_re = re.compile('(.)([A-Z])') + + +def die(msg, e=None): + if e: + msg = "%s (%s)" % (msg, e) + sys.stderr.write(msg + "\n") + sys.exit(1) + + +def what_to_cls(what): + return "".join([s.capitalize() for s in what.split("-")]) + + +def cls_to_what(cls): + return camel_re.sub(r'\1-\2', cls.__name__).lower() + + +def _get_base_parser(): + parser = argparse.ArgumentParser( + description="GitLab API Command Line Interface") + parser.add_argument("--version", help="Display the version.", + action="store_true") + parser.add_argument("-v", "--verbose", "--fancy", + help="Verbose mode", + action="store_true") + parser.add_argument("-c", "--config-file", action='append', + help=("Configuration file to use. Can be used " + "multiple times.")) + parser.add_argument("-g", "--gitlab", + help=("Which configuration section should " + "be used. If not defined, the default selection " + "will be used."), + required=False) + + return parser + + +def _get_parser(cli_module): + parser = _get_base_parser() + return cli_module.extend_parser(parser) + + +def main(): + if "--version" in sys.argv: + print(gitlab.__version__) + exit(0) + + parser = _get_base_parser() + (options, args) = parser.parse_known_args(sys.argv) + + config = gitlab.config.GitlabConfigParser(options.gitlab, + options.config_file) + cli_module = importlib.import_module('gitlab.v%s.cli' % config.api_version) + parser = _get_parser(cli_module) + args = parser.parse_args(sys.argv[1:]) + config_files = args.config_file + gitlab_id = args.gitlab + verbose = args.verbose + action = args.action + what = args.what + + args = args.__dict__ + # Remove CLI behavior-related args + for item in ('gitlab', 'config_file', 'verbose', 'what', 'action', + 'version'): + args.pop(item) + args = {k: v for k, v in args.items() if v is not None} + + try: + gl = gitlab.Gitlab.from_config(gitlab_id, config_files) + gl.auth() + except Exception as e: + die(str(e)) + + cli_module.run(gl, what, action, args, verbose) + + sys.exit(0) diff --git a/gitlab/tests/test_cli.py b/gitlab/tests/test_cli.py index 701655d25..e6e290a4a 100644 --- a/gitlab/tests/test_cli.py +++ b/gitlab/tests/test_cli.py @@ -28,12 +28,13 @@ import unittest2 as unittest from gitlab import cli +import gitlab.v3.cli class TestCLI(unittest.TestCase): def test_what_to_cls(self): - self.assertEqual("Foo", cli._what_to_cls("foo")) - self.assertEqual("FooBar", cli._what_to_cls("foo-bar")) + self.assertEqual("Foo", cli.what_to_cls("foo")) + self.assertEqual("FooBar", cli.what_to_cls("foo-bar")) def test_cls_to_what(self): class Class(object): @@ -42,32 +43,33 @@ class Class(object): class TestClass(object): pass - self.assertEqual("test-class", cli._cls_to_what(TestClass)) - self.assertEqual("class", cli._cls_to_what(Class)) + self.assertEqual("test-class", cli.cls_to_what(TestClass)) + self.assertEqual("class", cli.cls_to_what(Class)) def test_die(self): with self.assertRaises(SystemExit) as test: - cli._die("foobar") + cli.die("foobar") self.assertEqual(test.exception.code, 1) - def test_extra_actions(self): - for cls, data in six.iteritems(cli.EXTRA_ACTIONS): - for key in data: - self.assertIsInstance(data[key], dict) - - def test_parsing(self): - args = cli._parse_args(['-v', '-g', 'gl_id', - '-c', 'foo.cfg', '-c', 'bar.cfg', - 'project', 'list']) + def test_base_parser(self): + parser = cli._get_base_parser() + args = parser.parse_args(['-v', '-g', 'gl_id', + '-c', 'foo.cfg', '-c', 'bar.cfg']) self.assertTrue(args.verbose) self.assertEqual(args.gitlab, 'gl_id') self.assertEqual(args.config_file, ['foo.cfg', 'bar.cfg']) + + +class TestV3CLI(unittest.TestCase): + def test_parse_args(self): + parser = cli._get_parser(gitlab.v3.cli) + args = parser.parse_args(['project', 'list']) self.assertEqual(args.what, 'project') self.assertEqual(args.action, 'list') def test_parser(self): - parser = cli._build_parser() + parser = cli._get_parser(gitlab.v3.cli) subparsers = None for action in parser._actions: if type(action) == argparse._SubParsersAction: @@ -93,3 +95,8 @@ def test_parser(self): actions = user_subparsers.choices['create']._option_string_actions self.assertFalse(actions['--twitter'].required) self.assertTrue(actions['--username'].required) + + def test_extra_actions(self): + for cls, data in six.iteritems(gitlab.v3.cli.EXTRA_ACTIONS): + for key in data: + self.assertIsInstance(data[key], dict) diff --git a/gitlab/v3/cli.py b/gitlab/v3/cli.py index 142ccfa4d..b0450e8bf 100644 --- a/gitlab/v3/cli.py +++ b/gitlab/v3/cli.py @@ -18,145 +18,124 @@ from __future__ import print_function from __future__ import absolute_import -import argparse import inspect import operator -import re import sys import six import gitlab +import gitlab.base +from gitlab import cli +import gitlab.v3.objects -camel_re = re.compile('(.)([A-Z])') EXTRA_ACTIONS = { - gitlab.Group: {'search': {'required': ['query']}}, - gitlab.ProjectBranch: {'protect': {'required': ['id', 'project-id']}, - 'unprotect': {'required': ['id', 'project-id']}}, - gitlab.ProjectBuild: {'cancel': {'required': ['id', 'project-id']}, - 'retry': {'required': ['id', 'project-id']}, - 'artifacts': {'required': ['id', 'project-id']}, - 'trace': {'required': ['id', 'project-id']}}, - gitlab.ProjectCommit: {'diff': {'required': ['id', 'project-id']}, - 'blob': {'required': ['id', 'project-id', - 'filepath']}, - 'builds': {'required': ['id', 'project-id']}, - 'cherrypick': {'required': ['id', 'project-id', - 'branch']}}, - gitlab.ProjectIssue: {'subscribe': {'required': ['id', 'project-id']}, - 'unsubscribe': {'required': ['id', 'project-id']}, - 'move': {'required': ['id', 'project-id', - 'to-project-id']}}, - gitlab.ProjectMergeRequest: { + gitlab.v3.objects.Group: { + 'search': {'required': ['query']}}, + gitlab.v3.objects.ProjectBranch: { + 'protect': {'required': ['id', 'project-id']}, + 'unprotect': {'required': ['id', 'project-id']}}, + gitlab.v3.objects.ProjectBuild: { + 'cancel': {'required': ['id', 'project-id']}, + 'retry': {'required': ['id', 'project-id']}, + 'artifacts': {'required': ['id', 'project-id']}, + 'trace': {'required': ['id', 'project-id']}}, + gitlab.v3.objects.ProjectCommit: { + 'diff': {'required': ['id', 'project-id']}, + 'blob': {'required': ['id', 'project-id', 'filepath']}, + 'builds': {'required': ['id', 'project-id']}, + 'cherrypick': {'required': ['id', 'project-id', 'branch']}}, + gitlab.v3.objects.ProjectIssue: { + 'subscribe': {'required': ['id', 'project-id']}, + 'unsubscribe': {'required': ['id', 'project-id']}, + 'move': {'required': ['id', 'project-id', 'to-project-id']}}, + gitlab.v3.objects.ProjectMergeRequest: { 'closes-issues': {'required': ['id', 'project-id']}, 'cancel': {'required': ['id', 'project-id']}, 'merge': {'required': ['id', 'project-id'], 'optional': ['merge-commit-message', 'should-remove-source-branch', - 'merged-when-build-succeeds']} - }, - gitlab.ProjectMilestone: {'issues': {'required': ['id', 'project-id']}}, - gitlab.Project: {'search': {'required': ['query']}, - 'owned': {}, - 'all': {'optional': [('all', bool)]}, - 'starred': {}, - 'star': {'required': ['id']}, - 'unstar': {'required': ['id']}, - 'archive': {'required': ['id']}, - 'unarchive': {'required': ['id']}, - 'share': {'required': ['id', 'group-id', - 'group-access']}}, - gitlab.User: {'block': {'required': ['id']}, - 'unblock': {'required': ['id']}, - 'search': {'required': ['query']}, - 'get-by-username': {'required': ['query']}}, + 'merged-when-build-succeeds']}}, + gitlab.v3.objects.ProjectMilestone: { + 'issues': {'required': ['id', 'project-id']}}, + gitlab.v3.objects.Project: { + 'search': {'required': ['query']}, + 'owned': {}, + 'all': {'optional': [('all', bool)]}, + 'starred': {}, + 'star': {'required': ['id']}, + 'unstar': {'required': ['id']}, + 'archive': {'required': ['id']}, + 'unarchive': {'required': ['id']}, + 'share': {'required': ['id', 'group-id', 'group-access']}}, + gitlab.v3.objects.User: { + 'block': {'required': ['id']}, + 'unblock': {'required': ['id']}, + 'search': {'required': ['query']}, + 'get-by-username': {'required': ['query']}}, } -def _die(msg, e=None): - if e: - msg = "%s (%s)" % (msg, e) - sys.stderr.write(msg + "\n") - sys.exit(1) - - -def _what_to_cls(what): - return "".join([s.capitalize() for s in what.split("-")]) - - -def _cls_to_what(cls): - return camel_re.sub(r'\1-\2', cls.__name__).lower() - - -def do_auth(gitlab_id, config_files): - try: - gl = gitlab.Gitlab.from_config(gitlab_id, config_files) - gl.auth() - return gl - except Exception as e: - _die(str(e)) - - class GitlabCLI(object): def _get_id(self, cls, args): try: id = args.pop(cls.idAttr) except Exception: - _die("Missing --%s argument" % cls.idAttr.replace('_', '-')) + cli.die("Missing --%s argument" % cls.idAttr.replace('_', '-')) return id def do_create(self, cls, gl, what, args): if not cls.canCreate: - _die("%s objects can't be created" % what) + cli.die("%s objects can't be created" % what) try: o = cls.create(gl, args) except Exception as e: - _die("Impossible to create object", e) + cli.die("Impossible to create object", e) return o def do_list(self, cls, gl, what, args): if not cls.canList: - _die("%s objects can't be listed" % what) + cli.die("%s objects can't be listed" % what) try: l = cls.list(gl, **args) except Exception as e: - _die("Impossible to list objects", e) + cli.die("Impossible to list objects", e) return l def do_get(self, cls, gl, what, args): if cls.canGet is False: - _die("%s objects can't be retrieved" % what) + cli.die("%s objects can't be retrieved" % what) id = None - if cls not in [gitlab.CurrentUser] and cls.getRequiresId: + if cls not in [gitlab.v3.objects.CurrentUser] and cls.getRequiresId: id = self._get_id(cls, args) try: o = cls.get(gl, id, **args) except Exception as e: - _die("Impossible to get object", e) + cli.die("Impossible to get object", e) return o def do_delete(self, cls, gl, what, args): if not cls.canDelete: - _die("%s objects can't be deleted" % what) + cli.die("%s objects can't be deleted" % what) id = args.pop(cls.idAttr) try: gl.delete(cls, id, **args) except Exception as e: - _die("Impossible to destroy object", e) + cli.die("Impossible to destroy object", e) def do_update(self, cls, gl, what, args): if not cls.canUpdate: - _die("%s objects can't be updated" % what) + cli.die("%s objects can't be updated" % what) o = self.do_get(cls, gl, what, args) try: @@ -164,7 +143,7 @@ def do_update(self, cls, gl, what, args): o.__dict__[k] = v o.save() except Exception as e: - _die("Impossible to update object", e) + cli.die("Impossible to update object", e) return o @@ -172,171 +151,171 @@ def do_group_search(self, cls, gl, what, args): try: return gl.groups.search(args['query']) except Exception as e: - _die("Impossible to search projects", e) + cli.die("Impossible to search projects", e) def do_project_search(self, cls, gl, what, args): try: return gl.projects.search(args['query']) except Exception as e: - _die("Impossible to search projects", e) + cli.die("Impossible to search projects", e) def do_project_all(self, cls, gl, what, args): try: return gl.projects.all(all=args.get('all', False)) except Exception as e: - _die("Impossible to list all projects", e) + cli.die("Impossible to list all projects", e) def do_project_starred(self, cls, gl, what, args): try: return gl.projects.starred() except Exception as e: - _die("Impossible to list starred projects", e) + cli.die("Impossible to list starred projects", e) def do_project_owned(self, cls, gl, what, args): try: return gl.projects.owned() except Exception as e: - _die("Impossible to list owned projects", e) + cli.die("Impossible to list owned projects", e) def do_project_star(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.star() except Exception as e: - _die("Impossible to star project", e) + cli.die("Impossible to star project", e) def do_project_unstar(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.unstar() except Exception as e: - _die("Impossible to unstar project", e) + cli.die("Impossible to unstar project", e) def do_project_archive(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.archive_() except Exception as e: - _die("Impossible to archive project", e) + cli.die("Impossible to archive project", e) def do_project_unarchive(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.unarchive_() except Exception as e: - _die("Impossible to unarchive project", e) + cli.die("Impossible to unarchive project", e) def do_project_share(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.share(args['group_id'], args['group_access']) except Exception as e: - _die("Impossible to share project", e) + cli.die("Impossible to share project", e) def do_user_block(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.block() except Exception as e: - _die("Impossible to block user", e) + cli.die("Impossible to block user", e) def do_user_unblock(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.unblock() except Exception as e: - _die("Impossible to block user", e) + cli.die("Impossible to block user", e) def do_project_commit_diff(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return [x['diff'] for x in o.diff()] except Exception as e: - _die("Impossible to get commit diff", e) + cli.die("Impossible to get commit diff", e) def do_project_commit_blob(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.blob(args['filepath']) except Exception as e: - _die("Impossible to get commit blob", e) + cli.die("Impossible to get commit blob", e) def do_project_commit_builds(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.builds() except Exception as e: - _die("Impossible to get commit builds", e) + cli.die("Impossible to get commit builds", e) def do_project_commit_cherrypick(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.cherry_pick(branch=args['branch']) except Exception as e: - _die("Impossible to cherry-pick commit", e) + cli.die("Impossible to cherry-pick commit", e) def do_project_build_cancel(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.cancel() except Exception as e: - _die("Impossible to cancel project build", e) + cli.die("Impossible to cancel project build", e) def do_project_build_retry(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.retry() except Exception as e: - _die("Impossible to retry project build", e) + cli.die("Impossible to retry project build", e) def do_project_build_artifacts(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.artifacts() except Exception as e: - _die("Impossible to get project build artifacts", e) + cli.die("Impossible to get project build artifacts", e) def do_project_build_trace(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.trace() except Exception as e: - _die("Impossible to get project build trace", e) + cli.die("Impossible to get project build trace", e) def do_project_issue_subscribe(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.subscribe() except Exception as e: - _die("Impossible to subscribe to issue", e) + cli.die("Impossible to subscribe to issue", e) def do_project_issue_unsubscribe(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.unsubscribe() except Exception as e: - _die("Impossible to subscribe to issue", e) + cli.die("Impossible to subscribe to issue", e) def do_project_issue_move(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.move(args['to_project_id']) except Exception as e: - _die("Impossible to move issue", e) + cli.die("Impossible to move issue", e) def do_project_merge_request_closesissues(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.closes_issues() except Exception as e: - _die("Impossible to list issues closed by merge request", e) + cli.die("Impossible to list issues closed by merge request", e) def do_project_merge_request_cancel(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.cancel_merge_when_build_succeeds() except Exception as e: - _die("Impossible to cancel merge request", e) + cli.die("Impossible to cancel merge request", e) def do_project_merge_request_merge(self, cls, gl, what, args): try: @@ -348,26 +327,26 @@ def do_project_merge_request_merge(self, cls, gl, what, args): should_remove_source_branch=should_remove, merged_when_build_succeeds=build_succeeds) except Exception as e: - _die("Impossible to validate merge request", e) + cli.die("Impossible to validate merge request", e) def do_project_milestone_issues(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.issues() except Exception as e: - _die("Impossible to get milestone issues", e) + cli.die("Impossible to get milestone issues", e) def do_user_search(self, cls, gl, what, args): try: return gl.users.search(args['query']) except Exception as e: - _die("Impossible to search users", e) + cli.die("Impossible to search users", e) def do_user_getbyusername(self, cls, gl, what, args): try: return gl.users.search(args['query']) except Exception as e: - _die("Impossible to get user %s" % args['query'], e) + cli.die("Impossible to get user %s" % args['query'], e) def _populate_sub_parser_by_class(cls, sub_parser): @@ -391,7 +370,7 @@ def _populate_sub_parser_by_class(cls, sub_parser): action='store_true') if action_name in ["get", "delete"]: - if cls not in [gitlab.CurrentUser]: + if cls not in [gitlab.v3.objects.CurrentUser]: if cls.getRequiresId: id_attr = cls.idAttr.replace('_', '-') sub_parser_action.add_argument("--%s" % id_attr, @@ -456,39 +435,23 @@ def _add_arg(parser, required, data): for arg in d.get('optional', [])] -def _build_parser(args=sys.argv[1:]): - parser = argparse.ArgumentParser( - description="GitLab API Command Line Interface") - parser.add_argument("--version", help="Display the version.", - action="store_true") - parser.add_argument("-v", "--verbose", "--fancy", - help="Verbose mode", - action="store_true") - parser.add_argument("-c", "--config-file", action='append', - help=("Configuration file to use. Can be used " - "multiple times.")) - parser.add_argument("-g", "--gitlab", - help=("Which configuration section should " - "be used. If not defined, the default selection " - "will be used."), - required=False) - +def extend_parser(parser): subparsers = parser.add_subparsers(title='object', dest='what', help="Object to manipulate.") subparsers.required = True # populate argparse for all Gitlab Object classes = [] - for cls in gitlab.__dict__.values(): + for cls in gitlab.v3.objects.__dict__.values(): try: - if gitlab.GitlabObject in inspect.getmro(cls): + if gitlab.base.GitlabObject in inspect.getmro(cls): classes.append(cls) except AttributeError: pass classes.sort(key=operator.attrgetter("__name__")) for cls in classes: - arg_name = _cls_to_what(cls) + arg_name = cli.cls_to_what(cls) object_group = subparsers.add_parser(arg_name) object_subparsers = object_group.add_subparsers( @@ -499,47 +462,19 @@ def _build_parser(args=sys.argv[1:]): return parser -def _parse_args(args=sys.argv[1:]): - parser = _build_parser() - return parser.parse_args(args) - - -def main(): - if "--version" in sys.argv: - print(gitlab.__version__) - exit(0) - - arg = _parse_args() - args = arg.__dict__ - - config_files = arg.config_file - gitlab_id = arg.gitlab - verbose = arg.verbose - action = arg.action - what = arg.what - - # Remove CLI behavior-related args - for item in ("gitlab", "config_file", "verbose", "what", "action", - "version"): - args.pop(item) - - args = {k: v for k, v in args.items() if v is not None} - - cls = None +def run(gl, what, action, args, verbose): try: - cls = gitlab.__dict__[_what_to_cls(what)] - except Exception: - _die("Unknown object: %s" % what) + cls = gitlab.v3.objects.__dict__[cli.what_to_cls(what)] + except ImportError: + cli.die("Unknown object: %s" % what) - gl = do_auth(gitlab_id, config_files) - - cli = GitlabCLI() + g_cli = GitlabCLI() method = None what = what.replace('-', '_') action = action.lower().replace('-', '') for test in ["do_%s_%s" % (what, action), "do_%s" % action]: - if hasattr(cli, test): + if hasattr(g_cli, test): method = test break @@ -547,7 +482,7 @@ def main(): sys.stderr.write("Don't know how to deal with this!\n") sys.exit(1) - ret_val = getattr(cli, method)(cls, gl, what, args) + ret_val = getattr(g_cli, method)(cls, gl, what, args) if isinstance(ret_val, list): for o in ret_val: @@ -556,9 +491,7 @@ def main(): print("") else: print(o) - elif isinstance(ret_val, gitlab.GitlabObject): + elif isinstance(ret_val, gitlab.base.GitlabObject): ret_val.display(verbose) elif isinstance(ret_val, six.string_types): print(ret_val) - - sys.exit(0)