Commit abea6e4d authored by Mark Beierl's avatar Mark Beierl
Browse files

Added missing source

parent 0b1c8147
Loading
Loading
Loading
Loading
Loading
+97 −0
Original line number Diff line number Diff line
# Copyright 2014-2015 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Bootstrap charm-helpers, installing its dependencies if necessary using
# only standard libraries.
from __future__ import print_function
from __future__ import absolute_import

import functools
import inspect
import subprocess
import sys

try:
    import six  # NOQA:F401
except ImportError:
    if sys.version_info.major == 2:
        subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
    else:
        subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
    import six  # NOQA:F401

try:
    import yaml  # NOQA:F401
except ImportError:
    if sys.version_info.major == 2:
        subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
    else:
        subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
    import yaml  # NOQA:F401


# Holds a list of mapping of mangled function names that have been deprecated
# using the @deprecate decorator below.  This is so that the warning is only
# printed once for each usage of the function.
__deprecated_functions = {}


def deprecate(warning, date=None, log=None):
    """Add a deprecation warning the first time the function is used.
    The date, which is a string in semi-ISO8660 format indicate the year-month
    that the function is officially going to be removed.

    usage:

    @deprecate('use core/fetch/add_source() instead', '2017-04')
    def contributed_add_source_thing(...):
        ...

    And it then prints to the log ONCE that the function is deprecated.
    The reason for passing the logging function (log) is so that hookenv.log
    can be used for a charm if needed.

    :param warning:  String to indicat where it has moved ot.
    :param date: optional sting, in YYYY-MM format to indicate when the
                 function will definitely (probably) be removed.
    :param log: The log function to call to log.  If not, logs to stdout
    """
    def wrap(f):

        @functools.wraps(f)
        def wrapped_f(*args, **kwargs):
            try:
                module = inspect.getmodule(f)
                file = inspect.getsourcefile(f)
                lines = inspect.getsourcelines(f)
                f_name = "{}-{}-{}..{}-{}".format(
                    module.__name__, file, lines[0], lines[-1], f.__name__)
            except (IOError, TypeError):
                # assume it was local, so just use the name of the function
                f_name = f.__name__
            if f_name not in __deprecated_functions:
                __deprecated_functions[f_name] = True
                s = "DEPRECATION WARNING: Function {} is being removed".format(
                    f.__name__)
                if date:
                    s = "{} on/around {}".format(s, date)
                if warning:
                    s = "{} : {}".format(s, warning)
                if log:
                    log(s)
                else:
                    print(s)
            return f(*args, **kwargs)
        return wrapped_f
    return wrap
+57 −0
Original line number Diff line number Diff line
==========
Commandant
==========

-----------------------------------------------------
Automatic command-line interfaces to Python functions
-----------------------------------------------------

One of the benefits of ``libvirt`` is the uniformity of the interface: the C  API (as well as the bindings in other languages) is a set of functions that accept parameters that are nearly identical to the command-line arguments.  If you run ``virsh``, you get an interactive command prompt that supports all of the same commands that your shell scripts use as ``virsh`` subcommands.

Command execution and stdio manipulation is the greatest common factor across all development systems in the POSIX environment.  By exposing your functions as commands that manipulate streams of text, you can make life easier for all the Ruby and Erlang and Go programmers in your life.

Goals
=====

* Single decorator to expose a function as a command.
  * now two decorators - one "automatic" and one that allows authors to manipulate the arguments for fine-grained control.(MW)
* Automatic analysis of function signature through ``inspect.getargspec()``
* Command argument parser built automatically with ``argparse``
* Interactive interpreter loop object made with ``Cmd``
* Options to output structured return value data via ``pprint``, ``yaml`` or ``json`` dumps.

Other Important Features that need writing
------------------------------------------

* Help and Usage documentation can be automatically generated, but it will be important to let users override this behaviour
* The decorator should allow specifying further parameters to the parser's add_argument() calls, to specify types or to make arguments behave as boolean flags, etc.
    - Filename arguments are important, as good practice is for functions to accept file objects as parameters.
    - choices arguments help to limit bad input before the function is called
* Some automatic behaviour could make for better defaults, once the user can override them.
    - We could automatically detect arguments that default to False or True, and automatically support --no-foo for foo=True.
    - We could automatically support hyphens as alternates for underscores
    - Arguments defaulting to sequence types could support the ``append`` action.


-----------------------------------------------------
Implementing subcommands
-----------------------------------------------------

(WIP)

So as to avoid dependencies on the cli module, subcommands should be defined separately from their implementations. The recommmendation would be to place definitions into separate modules near the implementations which they expose.

Some examples::

    from charmhelpers.cli import CommandLine
    from charmhelpers.payload import execd
    from charmhelpers.foo import bar

    cli = CommandLine()

    cli.subcommand(execd.execd_run)

    @cli.subcommand_builder("bar", help="Bar baz qux")
    def barcmd_builder(subparser):
        subparser.add_argument('argument1', help="yackety")
        return bar
+189 −0
Original line number Diff line number Diff line
# Copyright 2014-2015 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import inspect
import argparse
import sys

from six.moves import zip

import charmhelpers.core.unitdata


class OutputFormatter(object):
    def __init__(self, outfile=sys.stdout):
        self.formats = (
            "raw",
            "json",
            "py",
            "yaml",
            "csv",
            "tab",
        )
        self.outfile = outfile

    def add_arguments(self, argument_parser):
        formatgroup = argument_parser.add_mutually_exclusive_group()
        choices = self.supported_formats
        formatgroup.add_argument("--format", metavar='FMT',
                                 help="Select output format for returned data, "
                                      "where FMT is one of: {}".format(choices),
                                 choices=choices, default='raw')
        for fmt in self.formats:
            fmtfunc = getattr(self, fmt)
            formatgroup.add_argument("-{}".format(fmt[0]),
                                     "--{}".format(fmt), action='store_const',
                                     const=fmt, dest='format',
                                     help=fmtfunc.__doc__)

    @property
    def supported_formats(self):
        return self.formats

    def raw(self, output):
        """Output data as raw string (default)"""
        if isinstance(output, (list, tuple)):
            output = '\n'.join(map(str, output))
        self.outfile.write(str(output))

    def py(self, output):
        """Output data as a nicely-formatted python data structure"""
        import pprint
        pprint.pprint(output, stream=self.outfile)

    def json(self, output):
        """Output data in JSON format"""
        import json
        json.dump(output, self.outfile)

    def yaml(self, output):
        """Output data in YAML format"""
        import yaml
        yaml.safe_dump(output, self.outfile)

    def csv(self, output):
        """Output data as excel-compatible CSV"""
        import csv
        csvwriter = csv.writer(self.outfile)
        csvwriter.writerows(output)

    def tab(self, output):
        """Output data in excel-compatible tab-delimited format"""
        import csv
        csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
        csvwriter.writerows(output)

    def format_output(self, output, fmt='raw'):
        fmtfunc = getattr(self, fmt)
        fmtfunc(output)


class CommandLine(object):
    argument_parser = None
    subparsers = None
    formatter = None
    exit_code = 0

    def __init__(self):
        if not self.argument_parser:
            self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
        if not self.formatter:
            self.formatter = OutputFormatter()
            self.formatter.add_arguments(self.argument_parser)
        if not self.subparsers:
            self.subparsers = self.argument_parser.add_subparsers(help='Commands')

    def subcommand(self, command_name=None):
        """
        Decorate a function as a subcommand. Use its arguments as the
        command-line arguments"""
        def wrapper(decorated):
            cmd_name = command_name or decorated.__name__
            subparser = self.subparsers.add_parser(cmd_name,
                                                   description=decorated.__doc__)
            for args, kwargs in describe_arguments(decorated):
                subparser.add_argument(*args, **kwargs)
            subparser.set_defaults(func=decorated)
            return decorated
        return wrapper

    def test_command(self, decorated):
        """
        Subcommand is a boolean test function, so bool return values should be
        converted to a 0/1 exit code.
        """
        decorated._cli_test_command = True
        return decorated

    def no_output(self, decorated):
        """
        Subcommand is not expected to return a value, so don't print a spurious None.
        """
        decorated._cli_no_output = True
        return decorated

    def subcommand_builder(self, command_name, description=None):
        """
        Decorate a function that builds a subcommand. Builders should accept a
        single argument (the subparser instance) and return the function to be
        run as the command."""
        def wrapper(decorated):
            subparser = self.subparsers.add_parser(command_name)
            func = decorated(subparser)
            subparser.set_defaults(func=func)
            subparser.description = description or func.__doc__
        return wrapper

    def run(self):
        "Run cli, processing arguments and executing subcommands."
        arguments = self.argument_parser.parse_args()
        argspec = inspect.getargspec(arguments.func)
        vargs = []
        for arg in argspec.args:
            vargs.append(getattr(arguments, arg))
        if argspec.varargs:
            vargs.extend(getattr(arguments, argspec.varargs))
        output = arguments.func(*vargs)
        if getattr(arguments.func, '_cli_test_command', False):
            self.exit_code = 0 if output else 1
            output = ''
        if getattr(arguments.func, '_cli_no_output', False):
            output = ''
        self.formatter.format_output(output, arguments.format)
        if charmhelpers.core.unitdata._KV:
            charmhelpers.core.unitdata._KV.flush()


cmdline = CommandLine()


def describe_arguments(func):
    """
    Analyze a function's signature and return a data structure suitable for
    passing in as arguments to an argparse parser's add_argument() method."""

    argspec = inspect.getargspec(func)
    # we should probably raise an exception somewhere if func includes **kwargs
    if argspec.defaults:
        positional_args = argspec.args[:-len(argspec.defaults)]
        keyword_names = argspec.args[-len(argspec.defaults):]
        for arg, default in zip(keyword_names, argspec.defaults):
            yield ('--{}'.format(arg),), {'default': default}
    else:
        positional_args = argspec.args

    for arg in positional_args:
        yield (arg,), {}
    if argspec.varargs:
        yield (argspec.varargs,), {'nargs': '*'}
+34 −0
Original line number Diff line number Diff line
# Copyright 2014-2015 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from . import cmdline
from charmhelpers.contrib.benchmark import Benchmark


@cmdline.subcommand(command_name='benchmark-start')
def start():
    Benchmark.start()


@cmdline.subcommand(command_name='benchmark-finish')
def finish():
    Benchmark.finish()


@cmdline.subcommand_builder('benchmark-composite', description="Set the benchmark composite score")
def service(subparser):
    subparser.add_argument("value", help="The composite score.")
    subparser.add_argument("units", help="The units the composite score represents, i.e., 'reads/sec'.")
    subparser.add_argument("direction", help="'asc' if a lower score is better, 'desc' if a higher score is better.")
    return Benchmark.set_composite_score
+30 −0
Original line number Diff line number Diff line
# Copyright 2014-2015 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
This module loads sub-modules into the python runtime so they can be
discovered via the inspect module. In order to prevent flake8 from (rightfully)
telling us these are unused modules, throw a ' # noqa' at the end of each import
so that the warning is suppressed.
"""

from . import CommandLine  # noqa

"""
Import the sub-modules which have decorated subcommands to register with chlp.
"""
from . import host  # noqa
from . import benchmark  # noqa
from . import unitdata  # noqa
from . import hookenv  # noqa
Loading