Skip to content

Commit

Permalink
- osc.cli.cli: added support for textual aliases
Browse files Browse the repository at this point in the history
- osc.cli.parse: moved parsing related code from osc.cli.cli into a
  new parse module

A textual alias is a user specified alias for an existing command.
It can be specified as follows:

class ReviewListGroupAlias(TextualAlias, ReviewList):
    """Alias for "review list --group opensuse-review-team"

    Examples:
    osc review glist api://
    # also show declined states
    osc review glist api://openSUSE:Factory -s declined

    """
    cmd = 'glist'
    alias = 'review list --group opensuse-review-team'

Afterwards it can be executed via "osc review glist api://openSUSE:Factory".
  • Loading branch information
marcus-h committed Oct 5, 2012
1 parent 2a6d519 commit e3e424c
Show file tree
Hide file tree
Showing 2 changed files with 192 additions and 107 deletions.
157 changes: 50 additions & 107 deletions osc/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,97 +5,10 @@
import logging
from ConfigParser import SafeConfigParser

import argparse

from osc.core import Osc
from osc.oscargs import OscArgs
from osc.cli.description import CommandDescription
from osc.cli import render


class CustomOscArgs(OscArgs):
"""A custom OscArgs class.
Adds "unresolved" arguments to the info object.
"""
def unresolved(self, info, name):
info.add(name, None)


class _OscNamespace(argparse.Namespace):
"""Resolves osc url-like arguments."""

def _path(self):
"""Returns a path.
If the command is not context sensitive None
is returned.
"""
path = None
if self.oargs_use_wc:
path = os.getcwd()
return path

def _add_items(self, info):
"""Add parsed items to the info object."""
if self.func_defaults is not None:
for k, v in self.func_defaults.iteritems():
info.add(k, v)
# add options etc. to info object
for i in self.__dict__.keys():
if (i.startswith('oargs') or i in info or i in self.oargs
or i == 'func_defaults'):
continue
elif i.startswith('opt_oargs_'):
self._resolve_option(info, i)
else:
info.add(i, getattr(self, i))

def _resolve_option(self, info, opt):
"""Resolve parsable option
info is the info object and opt an
attribute of the info object.
"""
name = opt.split('_', 2)[2]
args = getattr(self, name) # specified options (by the user)
format_entries = getattr(self, opt)
if not hasattr(args, 'extend'):
msg = ('list expected: please set "nargs" in the option '
'definition and/or default=[]')
raise ValueError(msg)
if not args:
# no args specified - nothing to parse
return
# check if the option was defined with action='append'
if not hasattr(args[0], 'extend'):
# that is option was only specified once (no action='append')
# it should also hold len(args) == len(format_entries)
args = [args]
for arg in args:
if len(arg) % len(format_entries) != 0:
msg = ('unexpected args len: the option args should be an '
'integer multiple of the number of specified '
'format_entries')
raise ValueError(msg)
# everything looks good - start parsing
res = []
oargs = CustomOscArgs(*format_entries)
while args:
cur = args.pop(0)
res.append(oargs.resolve(*cur, path=self._path()))
info.add(name, res)

def resolve(self):
"""Resolve osc url-like arguments."""
args = [getattr(self, k, '') for k in self.oargs]
oargs = CustomOscArgs(*self.oargs)
info = oargs.resolve(*args, path=self._path())
self._add_items(info)
return info
from osc.cli import parse


# TODO: move this into a different module
Expand Down Expand Up @@ -236,24 +149,59 @@ def renderer():
return renderer.renderer


def _parser():
"""Sets up and returns a new ArgumentParser object."""
parser = argparse.ArgumentParser(description=OscCommand.__doc__)
OscCommand.add_arguments(parser)
return parser
def execute_alias(cmd, args):
"""Executes the "cmd args".
cmd is the command and args are optional (user specified)
arguments.
"""
if hasattr(args, 'extend'):
args = ' '.join(args)
cmd = "%s %s" % (cmd, args)
execute(tuple(cmd.split()))


class TextualAlias(object):
"""This class can be used to define a textual alias.
A textual alias is an alias for an existing command + options.
In order to define a textual alias a new class has to be created
and has to inherit from this class and from OscCommand or a subclass.
def _parse():
"""Parses arguments from sys.stdin.
It is important that this class precedes the OscCommand (or subclass)
class in the linearization of the new class. Otherwise this class'
add_arguments method is not called.
Example:
class Correct(TextualAlias, SomeOscCommand): pass
class Wrong(SomeOscCommand, TextualAlias): pass
"""

args = '(plain_args)R'
alias = ''
func = call(execute_alias)

@classmethod
def add_arguments(cls, parser):
cls.func_defaults = {'cmd': cls.alias}
super(TextualAlias, cls).add_arguments(parser)

An osc.oscargs.ResolvedInfo object is returned. If the
passed arguments cannot be resolved a ValueError is raised.

def execute(args=None):
"""Executes a command specified by args.
Keyword arguments:
args -- represents the command to be executed (default: None
that is the command is read from stdin)
"""
parser = _parser()
ns = _OscNamespace()
parser.parse_args(namespace=ns)
return ns.resolve()
info = parse.parse(OscCommand, args)
apiurl = 'api'
if 'apiurl' in info:
apiurl = info.apiurl
info.set('apiurl', _init(apiurl))
info.func(info)


if __name__ == '__main__':
Expand All @@ -266,9 +214,4 @@ def _parse():
logging.getLogger('osc.cli.request.request').setLevel(logging.DEBUG)
logging.getLogger('osc.cli.review.review').addHandler(logger)
logging.getLogger('osc.cli.review.review').setLevel(logging.DEBUG)
info = _parse()
apiurl = 'api'
if 'apiurl' in info:
apiurl = info.apiurl
info.add('apiurl', _init(apiurl))
info.func(info)
execute()
142 changes: 142 additions & 0 deletions osc/cli/parse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Provides methods and classes for parsing the commandline options."""

import os

import argparse

from osc.oscargs import OscArgs
from osc.core import Osc


class CustomOscArgs(OscArgs):
"""A custom OscArgs class.
Adds "unresolved" arguments to the info object.
"""
def unresolved(self, info, name):
info.add(name, None)


class _OscNamespace(argparse.Namespace):
"""Resolves osc url-like arguments."""

def _path(self):
"""Returns a path.
If the command is not context sensitive None
is returned.
"""
path = None
if self.oargs_use_wc:
path = os.getcwd()
return path

def _add_items(self, info):
"""Add parsed items to the info object."""
if self.func_defaults is not None:
for k, v in self.func_defaults.iteritems():
info.add(k, v)
# add options etc. to info object
for i in self.__dict__.keys():
if (i.startswith('oargs') or i in info or i in self.oargs
or i == 'func_defaults'):
continue
elif i.startswith('opt_oargs_'):
self._resolve_option(info, i)
else:
info.add(i, getattr(self, i))

def _resolve_option(self, info, opt):
"""Resolve parsable option
info is the info object and opt an
attribute of the info object.
"""
name = opt.split('_', 2)[2]
args = getattr(self, name) # specified options (by the user)
format_entries = getattr(self, opt)
if not hasattr(args, 'extend'):
msg = ('list expected: please set "nargs" in the option '
'definition and/or default=[]')
raise ValueError(msg)
if not args:
# no args specified - nothing to parse
return
# check if the option was defined with action='append'
if not hasattr(args[0], 'extend'):
# that is option was only specified once (no action='append')
# it should also hold len(args) == len(format_entries)
args = [args]
for arg in args:
if len(arg) % len(format_entries) != 0:
msg = ('unexpected args len: the option args should be an '
'integer multiple of the number of specified '
'format_entries')
raise ValueError(msg)
# everything looks good - start parsing
res = []
oargs = CustomOscArgs(*format_entries)
while args:
cur = args.pop(0)
res.append(oargs.resolve(*cur, path=self._path()))
info.add(name, res)

def _resolve_positional_args(self):
"""Resolve positional arguments.
A ResolvedInfo object is returned. If it is not
possible to resolve the positional arguments a
ValueError is raised.
"""
args = []
format_entries = []
for oarg in self.oargs:
val = getattr(self, oarg, '')
if hasattr(val, 'extend'):
# len(val) arguments of "type" oarg
# were specified by the user
format_entries.extend([oarg] * len(val))
args.extend(val)
else:
format_entries.append(oarg)
args.append(val)
oargs = CustomOscArgs(*format_entries, ignore_clashes=False)
return oargs.resolve(*args, path=self._path())

def resolve(self):
"""Resolve osc url-like arguments."""
info = self._resolve_positional_args()
self._add_items(info)
return info


def _parser(root_cmd_cls):
"""Sets up and returns a new ArgumentParser object.
root_cmd_cls specifies the root command class which is
used to initialize the parser.
"""
parser = argparse.ArgumentParser(description=root_cmd_cls.__doc__)
root_cmd_cls.add_arguments(parser)
return parser


def parse(root_cmd_cls, args):
"""Parses arguments specified by args
If args is None sys.stdin is used. root_cmd_cls specifies
the root command class which is used for setting up the
argparse parser.
An osc.oscargs.ResolvedInfo object is returned. If the
passed arguments cannot be resolved a ValueError is raised.
"""
parser = _parser(root_cmd_cls)
ns = _OscNamespace()
parser.parse_args(args=args, namespace=ns)
return ns.resolve()

0 comments on commit e3e424c

Please sign in to comment.