Add --format to the client shell
Add a new way of formatting our output in a consistent way. This turns print_list and print_dict into a formatter that has the same API as any other formatter and allows users to create their own formatters and plug them into cratonclient. This includes tests for the base level formatter and our two default formatters as well as some refactoring to allow users to specify their own --format. At the moment, however, the subcommand shells do *not* use the pluggable formatter decided by the user. That change and all of the downstream effects it has on testing is going to be *very* significant and deserves its own commit as this one is large enough. Change-Id: I6649ebce57d5ddf2d4aeb689e77e3c17ef3a2e97
This commit is contained in:
		
							
								
								
									
										12
									
								
								cratonclient/formatters/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								cratonclient/formatters/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
# 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.
 | 
			
		||||
"""Module containing built-in formatters for cratonclient CLI."""
 | 
			
		||||
							
								
								
									
										107
									
								
								cratonclient/formatters/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								cratonclient/formatters/base.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
			
		||||
# 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.
 | 
			
		||||
"""Base class implementation for formatting plugins."""
 | 
			
		||||
from cratonclient import crud
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Formatter(object):
 | 
			
		||||
    """Class that defines the formatter interface.
 | 
			
		||||
 | 
			
		||||
    Instead of having to override and call up to this class's ``__init__``
 | 
			
		||||
    method, we also provide an ``after_init`` method that can be implemented
 | 
			
		||||
    to extend what happens on initialization.
 | 
			
		||||
 | 
			
		||||
    .. attribute:: args
 | 
			
		||||
 | 
			
		||||
        Parsed command-line arguments stored as an instance of
 | 
			
		||||
        :class:`argparse.Namespace`.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, parsed_args):
 | 
			
		||||
        """Instantiate our formatter with the parsed CLI arguments.
 | 
			
		||||
 | 
			
		||||
        :param parsed_args:
 | 
			
		||||
            The CLI arguments parsed by :mod:`argparse`.
 | 
			
		||||
        :type parsed_args:
 | 
			
		||||
            argparse.Namespace
 | 
			
		||||
        """
 | 
			
		||||
        self.args = parsed_args
 | 
			
		||||
        self.after_init()
 | 
			
		||||
 | 
			
		||||
    def after_init(self):
 | 
			
		||||
        """Initialize the object further after ``__init__``."""
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def configure(self, *args, **kwargs):
 | 
			
		||||
        """Optional configuration of the plugin after instantiation."""
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    def handle(self, item_to_format):
 | 
			
		||||
        """Handle a returned item from the cratonclient API.
 | 
			
		||||
 | 
			
		||||
        cratonclient's API produces both single Resource objects as well as
 | 
			
		||||
        generators of those objects. This method should be capable of handling
 | 
			
		||||
        both.
 | 
			
		||||
 | 
			
		||||
        Based on the type, this will either call ``handle_generator`` or
 | 
			
		||||
        ``handle_instance``. Subclasses must implement both of those methods.
 | 
			
		||||
 | 
			
		||||
        :returns:
 | 
			
		||||
            None
 | 
			
		||||
        :rtype:
 | 
			
		||||
            None
 | 
			
		||||
        :raises ValueError:
 | 
			
		||||
            If the item provided is not a subclass of
 | 
			
		||||
            :class:`~cratonclient.crud.Resource` or an iterable class then
 | 
			
		||||
            we will not know how to handle it. In that case, we will raise a
 | 
			
		||||
            ValueError.
 | 
			
		||||
        """
 | 
			
		||||
        if isinstance(item_to_format, crud.Resource):
 | 
			
		||||
            self.handle_instance(item_to_format)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            self.handle_generator(item_to_format)
 | 
			
		||||
        except TypeError as err:
 | 
			
		||||
            raise ValueError(
 | 
			
		||||
                "Expected an iterable object but instead received something "
 | 
			
		||||
                "of type: %s. Received a TypeError: %s" % (
 | 
			
		||||
                    type(item_to_format),
 | 
			
		||||
                    err
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def handle_instance(self, instance):
 | 
			
		||||
        """Format and print the instance provided.
 | 
			
		||||
 | 
			
		||||
        :param instance:
 | 
			
		||||
            The instance retrieved from the API that needs to be formatted.
 | 
			
		||||
        :type instance:
 | 
			
		||||
            cratonclient.crud.Resource
 | 
			
		||||
        """
 | 
			
		||||
        raise NotImplementedError(
 | 
			
		||||
            "A formatter plugin subclassed Formatter but did not implement"
 | 
			
		||||
            " the handle_instance method."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def handle_generator(self, generator):
 | 
			
		||||
        """Format and print the instance provided.
 | 
			
		||||
 | 
			
		||||
        :param generator:
 | 
			
		||||
            The generator retrieved from the API whose items need to be
 | 
			
		||||
            formatted.
 | 
			
		||||
        """
 | 
			
		||||
        raise NotImplementedError(
 | 
			
		||||
            "A formatter plugin subclassed Formatter but did not implement"
 | 
			
		||||
            " the handle_generator method."
 | 
			
		||||
        )
 | 
			
		||||
							
								
								
									
										72
									
								
								cratonclient/formatters/json_format.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								cratonclient/formatters/json_format.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
			
		||||
# 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.
 | 
			
		||||
"""JSON formatter implementation for the craton CLI."""
 | 
			
		||||
from __future__ import print_function
 | 
			
		||||
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
from cratonclient.formatters import base
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Formatter(base.Formatter):
 | 
			
		||||
    """JSON output formatter for the CLI."""
 | 
			
		||||
 | 
			
		||||
    def after_init(self):
 | 
			
		||||
        """Set-up our defaults.
 | 
			
		||||
 | 
			
		||||
        At some point in the future, we may allow people to configure this via
 | 
			
		||||
        the CLI.
 | 
			
		||||
        """
 | 
			
		||||
        self.indent = 4
 | 
			
		||||
        self.sort_keys = True
 | 
			
		||||
 | 
			
		||||
    def format(self, dictionary):
 | 
			
		||||
        """Return the dictionary as a JSON string."""
 | 
			
		||||
        return json.dumps(
 | 
			
		||||
            dictionary,
 | 
			
		||||
            sort_keys=self.sort_keys,
 | 
			
		||||
            indent=self.indent,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def handle_instance(self, instance):
 | 
			
		||||
        """Print the JSON representation of a single instance."""
 | 
			
		||||
        print(self.format(instance.to_dict()))
 | 
			
		||||
 | 
			
		||||
    def handle_generator(self, generator):
 | 
			
		||||
        """Print the JSON representation of a collection."""
 | 
			
		||||
        # NOTE(sigmavirus24): This is tricky logic that is caused by the JSON
 | 
			
		||||
        # specification's intolerance for trailing commas.
 | 
			
		||||
        try:
 | 
			
		||||
            instance = next(generator)
 | 
			
		||||
        except StopIteration:
 | 
			
		||||
            # If there is nothing in the generator, we should just print an
 | 
			
		||||
            # empty Array and then exit immediately.
 | 
			
		||||
            print('[]')
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # Otherwise, let's print our opening bracket to start our Array
 | 
			
		||||
        # formatting.
 | 
			
		||||
        print('[', end='')
 | 
			
		||||
        while True:
 | 
			
		||||
            print(self.format(instance.to_dict()), end='')
 | 
			
		||||
            # After printing our instance as a JSON object, we need to
 | 
			
		||||
            # decide if we have another object to print. If we do have
 | 
			
		||||
            # another object to print, we need to print a comma to separate
 | 
			
		||||
            # our previous object and our next one. If we don't, we exit our
 | 
			
		||||
            # loop to print our closing Array bracket.
 | 
			
		||||
            try:
 | 
			
		||||
                instance = next(generator)
 | 
			
		||||
            except StopIteration:
 | 
			
		||||
                break
 | 
			
		||||
            else:
 | 
			
		||||
                print(', ', end='')
 | 
			
		||||
        print(']')
 | 
			
		||||
							
								
								
									
										180
									
								
								cratonclient/formatters/table.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								cratonclient/formatters/table.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,180 @@
 | 
			
		||||
# 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.
 | 
			
		||||
"""Pretty-table formatter implementation for the craton CLI."""
 | 
			
		||||
from __future__ import print_function
 | 
			
		||||
 | 
			
		||||
import textwrap
 | 
			
		||||
 | 
			
		||||
from oslo_utils import encodeutils
 | 
			
		||||
import prettytable
 | 
			
		||||
import six
 | 
			
		||||
 | 
			
		||||
from cratonclient.formatters import base
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Formatter(base.Formatter):
 | 
			
		||||
    """Implementation of the default table-style formatter."""
 | 
			
		||||
 | 
			
		||||
    def after_init(self):
 | 
			
		||||
        """Set-up after initialization."""
 | 
			
		||||
        self.fields = []
 | 
			
		||||
        self.formatters = {}
 | 
			
		||||
        self.sortby_index = 0
 | 
			
		||||
        self.mixed_case_fields = set([])
 | 
			
		||||
        self.field_labels = []
 | 
			
		||||
        self.dict_property = "Property"
 | 
			
		||||
        self.wrap = 0
 | 
			
		||||
        self.dict_value = "Value"
 | 
			
		||||
 | 
			
		||||
    def configure(self, fields=None, formatters=None, sortby_index=False,
 | 
			
		||||
                  mixed_case_fields=None, field_labels=None,
 | 
			
		||||
                  dict_property=None, dict_value=None, wrap=None):
 | 
			
		||||
        """Configure some of the settings used to print the tables.
 | 
			
		||||
 | 
			
		||||
        Parameters that configure list presentation:
 | 
			
		||||
 | 
			
		||||
        :param list fields:
 | 
			
		||||
            List of field names as strings.
 | 
			
		||||
        :param dict formatters:
 | 
			
		||||
            Mapping of field names to formatter functions that accept the
 | 
			
		||||
            resource.
 | 
			
		||||
        :param int sortby_index:
 | 
			
		||||
            The index of the field name in :param:`fields` to sort the table
 | 
			
		||||
            rows by. If ``None``, PrettyTable will not sort the items at all.
 | 
			
		||||
        :param list mixed_case_fields:
 | 
			
		||||
            List of field names also in :param:`fields` that are mixed case
 | 
			
		||||
            and need preprocessing prior to retrieving the attribute.
 | 
			
		||||
        :param list field_labels:
 | 
			
		||||
            List of field labels that need to match :param:`fields`.
 | 
			
		||||
 | 
			
		||||
        Parameters that configure the plain resource representation:
 | 
			
		||||
 | 
			
		||||
        :param str dict_property:
 | 
			
		||||
            The name of the first column.
 | 
			
		||||
        :param str dict_value:
 | 
			
		||||
            The name of the second column.
 | 
			
		||||
        :param int wrap:
 | 
			
		||||
            Length at which to wrap the second column.
 | 
			
		||||
 | 
			
		||||
        All of these may be specified, but will be ignored based on how the
 | 
			
		||||
        formatter is executed.
 | 
			
		||||
        """
 | 
			
		||||
        if fields is not None:
 | 
			
		||||
            self.fields = fields
 | 
			
		||||
        if field_labels is None:
 | 
			
		||||
            self.field_labels = self.fields
 | 
			
		||||
        elif len(field_labels) != len(self.fields):
 | 
			
		||||
            raise ValueError(
 | 
			
		||||
                "Field labels list %(labels)s has different number "
 | 
			
		||||
                "of elements than fields list %(fields)s" %
 | 
			
		||||
                {'labels': field_labels, 'fields': fields}
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            self.field_labels = field_labels
 | 
			
		||||
 | 
			
		||||
        if formatters is not None:
 | 
			
		||||
            self.formatters = formatters
 | 
			
		||||
 | 
			
		||||
        if sortby_index is not False:
 | 
			
		||||
            try:
 | 
			
		||||
                sortby_index = int(sortby_index)
 | 
			
		||||
            except TypeError:
 | 
			
		||||
                if sortby_index is not None:
 | 
			
		||||
                    raise ValueError(
 | 
			
		||||
                        'sortby_index must be None or an integer'
 | 
			
		||||
                    )
 | 
			
		||||
            except ValueError:
 | 
			
		||||
                raise
 | 
			
		||||
            else:
 | 
			
		||||
                if self.field_labels and (
 | 
			
		||||
                        sortby_index < 0 or
 | 
			
		||||
                        sortby_index > len(self.field_labels)):
 | 
			
		||||
                    raise ValueError(
 | 
			
		||||
                        'sortby_index must be a non-negative number less '
 | 
			
		||||
                        'than {}'.format(len(self.field_labels))
 | 
			
		||||
                    )
 | 
			
		||||
            self.sortby_index = sortby_index
 | 
			
		||||
 | 
			
		||||
        if mixed_case_fields is not None:
 | 
			
		||||
            self.mixed_case_fields = set(mixed_case_fields)
 | 
			
		||||
 | 
			
		||||
        if dict_property is not None:
 | 
			
		||||
            self.dict_property = dict_property
 | 
			
		||||
 | 
			
		||||
        if dict_value is not None:
 | 
			
		||||
            self.dict_value = dict_value
 | 
			
		||||
 | 
			
		||||
        if wrap is not None:
 | 
			
		||||
            self.wrap = wrap
 | 
			
		||||
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    def sortby_kwargs(self):
 | 
			
		||||
        """Generate the sortby keyword argument for PrettyTable."""
 | 
			
		||||
        if self.sortby_index is None:
 | 
			
		||||
            return {}
 | 
			
		||||
        return {'sortby': self.field_labels[self.sortby_index]}
 | 
			
		||||
 | 
			
		||||
    def build_table(self, field_labels, alignment='l'):
 | 
			
		||||
        """Create a PrettyTable instance based off of the labels."""
 | 
			
		||||
        table = prettytable.PrettyTable(field_labels)
 | 
			
		||||
        table.align = alignment
 | 
			
		||||
        return table
 | 
			
		||||
 | 
			
		||||
    def handle_generator(self, generator):
 | 
			
		||||
        """Handle a generator of resources."""
 | 
			
		||||
        sortby_kwargs = self.sortby_kwargs()
 | 
			
		||||
        table = self.build_table(self.field_labels)
 | 
			
		||||
 | 
			
		||||
        for resource in generator:
 | 
			
		||||
            row = []
 | 
			
		||||
            for field in self.fields:
 | 
			
		||||
                formatter = self.formatters.get(field)
 | 
			
		||||
                if formatter is not None:
 | 
			
		||||
                    data = formatter(resource)
 | 
			
		||||
                else:
 | 
			
		||||
                    if field in self.mixed_case_fields:
 | 
			
		||||
                        field_name = field.replace(' ', '_')
 | 
			
		||||
                    else:
 | 
			
		||||
                        field_name = field.lower().replace(' ', '_')
 | 
			
		||||
                    data = getattr(resource, field_name, '')
 | 
			
		||||
                row.append(data)
 | 
			
		||||
            table.add_row(row)
 | 
			
		||||
 | 
			
		||||
        output = encodeutils.safe_encode(table.get_string(**sortby_kwargs))
 | 
			
		||||
        if six.PY3:
 | 
			
		||||
            output = output.decode()
 | 
			
		||||
        print(output)
 | 
			
		||||
 | 
			
		||||
    def handle_instance(self, instance):
 | 
			
		||||
        """Handle a single resource."""
 | 
			
		||||
        table = self.build_table([self.dict_property, self.dict_value])
 | 
			
		||||
 | 
			
		||||
        for key, value in sorted(instance.to_dict().items()):
 | 
			
		||||
            if isinstance(value, dict):
 | 
			
		||||
                value = six.text_type(value)
 | 
			
		||||
            if self.wrap > 0:
 | 
			
		||||
                value = textwrap.fill(six.text_type(value), self.wrap)
 | 
			
		||||
 | 
			
		||||
            if value and isinstance(value, six.string_types) and '\n' in value:
 | 
			
		||||
                lines = value.strip().split('\n')
 | 
			
		||||
                column1 = key
 | 
			
		||||
                for line in lines:
 | 
			
		||||
                    table.add_row([column1, line])
 | 
			
		||||
                    column1 = ''
 | 
			
		||||
            else:
 | 
			
		||||
                table.add_row([key, value])
 | 
			
		||||
 | 
			
		||||
        output = encodeutils.safe_encode(table.get_string())
 | 
			
		||||
        if six.PY3:
 | 
			
		||||
            output = output.decode()
 | 
			
		||||
        print(output)
 | 
			
		||||
@@ -18,6 +18,7 @@ import sys
 | 
			
		||||
 | 
			
		||||
from oslo_utils import encodeutils
 | 
			
		||||
from oslo_utils import importutils
 | 
			
		||||
from stevedore import extension
 | 
			
		||||
 | 
			
		||||
from cratonclient import __version__
 | 
			
		||||
from cratonclient import exceptions as exc
 | 
			
		||||
@@ -27,11 +28,24 @@ from cratonclient.common import cliutils
 | 
			
		||||
from cratonclient.v1 import client
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FORMATTERS_NAMESPACE = 'cratonclient.formatters'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CratonShell(object):
 | 
			
		||||
    """Class used to handle shell definition and parsing."""
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_base_parser():
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        """Initialize our shell object.
 | 
			
		||||
 | 
			
		||||
        This sets up our formatters extension manager. If we add further
 | 
			
		||||
        managers, they will be initialized here.
 | 
			
		||||
        """
 | 
			
		||||
        self.extension_mgr = extension.ExtensionManager(
 | 
			
		||||
            namespace=FORMATTERS_NAMESPACE,
 | 
			
		||||
            invoke_on_load=False,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def get_base_parser(self):
 | 
			
		||||
        """Configure base craton arguments and parsing."""
 | 
			
		||||
        parser = argparse.ArgumentParser(
 | 
			
		||||
            prog='craton',
 | 
			
		||||
@@ -50,6 +64,13 @@ class CratonShell(object):
 | 
			
		||||
                            action='version',
 | 
			
		||||
                            version=__version__,
 | 
			
		||||
                            )
 | 
			
		||||
        parser.add_argument('--format',
 | 
			
		||||
                            default='default',
 | 
			
		||||
                            choices=list(sorted(self.extension_mgr.names())),
 | 
			
		||||
                            help='The format to use to print the information '
 | 
			
		||||
                                 'to the console. Defaults to pretty-printing '
 | 
			
		||||
                                 'using ASCII tables.',
 | 
			
		||||
                            )
 | 
			
		||||
        parser.add_argument('--craton-url',
 | 
			
		||||
                            default=cliutils.env('CRATON_URL'),
 | 
			
		||||
                            help='The base URL of the running Craton service.'
 | 
			
		||||
@@ -150,6 +171,8 @@ class CratonShell(object):
 | 
			
		||||
            project_id=args.os_project_id,
 | 
			
		||||
        )
 | 
			
		||||
        self.cc = client.Client(session, args.craton_url)
 | 
			
		||||
        formatter_class = self.extension_mgr[args.format].plugin
 | 
			
		||||
        args.formatter = formatter_class(args)
 | 
			
		||||
        args.func(self.cc, args)
 | 
			
		||||
 | 
			
		||||
    @cliutils.arg(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								cratonclient/tests/unit/formatters/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								cratonclient/tests/unit/formatters/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
# 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.
 | 
			
		||||
"""Test module for the cratonclient.formatters submodule."""
 | 
			
		||||
							
								
								
									
										74
									
								
								cratonclient/tests/unit/formatters/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								cratonclient/tests/unit/formatters/base.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
# 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.
 | 
			
		||||
"""Base TestCase class for Formatter tests."""
 | 
			
		||||
import argparse
 | 
			
		||||
 | 
			
		||||
import mock
 | 
			
		||||
import six
 | 
			
		||||
 | 
			
		||||
from cratonclient.tests import base
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FormatterTestCase(base.TestCase):
 | 
			
		||||
    """Base-level Formatter TestCase class."""
 | 
			
		||||
 | 
			
		||||
    def patch_stdout(self, autostart=True):
 | 
			
		||||
        """Patch sys.stdout and capture all output to it.
 | 
			
		||||
 | 
			
		||||
        This will automatically start the patch by default.
 | 
			
		||||
 | 
			
		||||
        :param bool autostart:
 | 
			
		||||
            Start patching sys.stdout immediately or delay that until later.
 | 
			
		||||
        """
 | 
			
		||||
        self.stdout_patcher = mock.patch('sys.stdout', new=six.StringIO())
 | 
			
		||||
        if autostart:
 | 
			
		||||
            self.stdout = self.stdout_patcher.start()
 | 
			
		||||
            self.addCleanup(self.unpatch_stdout)
 | 
			
		||||
 | 
			
		||||
    def unpatch_stdout(self):
 | 
			
		||||
        """Safely unpatch standard out."""
 | 
			
		||||
        if getattr(self.stdout_patcher, 'is_local', None) is not None:
 | 
			
		||||
            self.stdout_patcher.stop()
 | 
			
		||||
 | 
			
		||||
    def stripped_stdout(self):
 | 
			
		||||
        """Return the newline stripped standard-out captured string."""
 | 
			
		||||
        stdout = self.stdout.getvalue().rstrip('\n')
 | 
			
		||||
        self.unpatch_stdout()
 | 
			
		||||
        return stdout
 | 
			
		||||
 | 
			
		||||
    def args_for(self, **kwargs):
 | 
			
		||||
        """Return an instantiated argparse Namsepace.
 | 
			
		||||
 | 
			
		||||
        Using the specified keyword arguments, create and return a Namespace
 | 
			
		||||
        object from argparse for testing purposes.
 | 
			
		||||
        :returns:
 | 
			
		||||
            Instantiated namespace.
 | 
			
		||||
        :rtype:
 | 
			
		||||
            argparse.Namespace
 | 
			
		||||
        """
 | 
			
		||||
        return argparse.Namespace(**kwargs)
 | 
			
		||||
 | 
			
		||||
    def resource_info(self, **kwargs):
 | 
			
		||||
        """Return a dictionary with resource information.
 | 
			
		||||
 | 
			
		||||
        :returns:
 | 
			
		||||
            Dictionary with basic id and name as well as the provided keyword
 | 
			
		||||
            arguments.
 | 
			
		||||
        :rtype:
 | 
			
		||||
            dict
 | 
			
		||||
        """
 | 
			
		||||
        info = {
 | 
			
		||||
            'id': 1,
 | 
			
		||||
            'name': 'Test Resource',
 | 
			
		||||
        }
 | 
			
		||||
        info.update(kwargs)
 | 
			
		||||
        return info
 | 
			
		||||
							
								
								
									
										63
									
								
								cratonclient/tests/unit/formatters/test_base_formatter.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								cratonclient/tests/unit/formatters/test_base_formatter.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
# 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.
 | 
			
		||||
"""Tests for the base formatter class."""
 | 
			
		||||
import argparse
 | 
			
		||||
 | 
			
		||||
import mock
 | 
			
		||||
 | 
			
		||||
from cratonclient import crud
 | 
			
		||||
from cratonclient.formatters import base
 | 
			
		||||
from cratonclient.tests import base as testbase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestBaseFormatterInstantiation(testbase.TestCase):
 | 
			
		||||
    """Tests for cratonclient.formatters.base.Formatter creation."""
 | 
			
		||||
 | 
			
		||||
    def test_instantiation_calls_after_init(self):
 | 
			
		||||
        """Verify we call our postinitialization hook."""
 | 
			
		||||
        with mock.patch.object(base.Formatter, 'after_init') as after_init:
 | 
			
		||||
            base.Formatter(mock.Mock())
 | 
			
		||||
 | 
			
		||||
        after_init.assert_called_once_with()
 | 
			
		||||
 | 
			
		||||
    def test_stores_namespace_object(self):
 | 
			
		||||
        """Verify we store our parsed CLI arguments."""
 | 
			
		||||
        namespace = argparse.Namespace()
 | 
			
		||||
        formatter = base.Formatter(namespace)
 | 
			
		||||
        self.assertIs(namespace, formatter.args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestBaseFormatter(testbase.TestCase):
 | 
			
		||||
    """Tests for cratonclient.formatters.base.Formatter behaviour."""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        """Create test resources."""
 | 
			
		||||
        super(TestBaseFormatter, self).setUp()
 | 
			
		||||
        self.formatter = base.Formatter(argparse.Namespace())
 | 
			
		||||
 | 
			
		||||
    def test_handle_detects_resources(self):
 | 
			
		||||
        """Verify we handle instances explicitly."""
 | 
			
		||||
        resource = crud.Resource(mock.Mock(), {})
 | 
			
		||||
        method = 'handle_instance'
 | 
			
		||||
        with mock.patch.object(self.formatter, method) as handle_instance:
 | 
			
		||||
            self.formatter.handle(resource)
 | 
			
		||||
 | 
			
		||||
        handle_instance.assert_called_once_with(resource)
 | 
			
		||||
 | 
			
		||||
    def test_handle_detects_iterables(self):
 | 
			
		||||
        """Verify we handle generators explicitly."""
 | 
			
		||||
        method = 'handle_generator'
 | 
			
		||||
        iterable = iter([])
 | 
			
		||||
        with mock.patch.object(self.formatter, method) as handle_generator:
 | 
			
		||||
            self.formatter.handle(iterable)
 | 
			
		||||
 | 
			
		||||
        handle_generator.assert_called_once_with(iterable)
 | 
			
		||||
							
								
								
									
										69
									
								
								cratonclient/tests/unit/formatters/test_json_formatter.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								cratonclient/tests/unit/formatters/test_json_formatter.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
			
		||||
# 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.
 | 
			
		||||
"""JSON formatter unit tests."""
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
import mock
 | 
			
		||||
 | 
			
		||||
from cratonclient import crud
 | 
			
		||||
from cratonclient.formatters import json_format
 | 
			
		||||
from cratonclient.tests.unit.formatters import base
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestValidFormatting(base.FormatterTestCase):
 | 
			
		||||
    """Validate the JSON formatter's console output."""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        """Initialize our instance prior to each test."""
 | 
			
		||||
        super(TestValidFormatting, self).setUp()
 | 
			
		||||
        self.formatter = json_format.Formatter(self.args_for())
 | 
			
		||||
        self.patch_stdout()
 | 
			
		||||
 | 
			
		||||
    def load_json(self, stdout):
 | 
			
		||||
        """Load JSON data from standard-out capture.
 | 
			
		||||
 | 
			
		||||
        If there's an error decoding the JSON output, fail the test
 | 
			
		||||
        automatically.
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            return json.loads(stdout)
 | 
			
		||||
        except ValueError as err:
 | 
			
		||||
            self.fail('Encountered a ValueError: %s' % err)
 | 
			
		||||
 | 
			
		||||
    def test_instance_handling_creates_valid_json(self):
 | 
			
		||||
        """Verify the printed data is valid JSON."""
 | 
			
		||||
        info = self.resource_info()
 | 
			
		||||
        instance = crud.Resource(mock.Mock(), info, loaded=True)
 | 
			
		||||
        self.formatter.handle(instance)
 | 
			
		||||
        parsed = self.load_json(self.stripped_stdout())
 | 
			
		||||
        self.assertDictEqual(info, parsed)
 | 
			
		||||
 | 
			
		||||
    def test_empty_generator_handling(self):
 | 
			
		||||
        """Verify we simply print an empty list."""
 | 
			
		||||
        self.formatter.handle(iter([]))
 | 
			
		||||
        parsed = self.load_json(self.stripped_stdout())
 | 
			
		||||
        self.assertEqual([], parsed)
 | 
			
		||||
 | 
			
		||||
    def test_generator_of_a_single_resource(self):
 | 
			
		||||
        """Verify we print the single list appropriately."""
 | 
			
		||||
        info = self.resource_info()
 | 
			
		||||
        self.formatter.handle(iter([crud.Resource(mock.Mock(), info, True)]))
 | 
			
		||||
        parsed = self.load_json(self.stripped_stdout())
 | 
			
		||||
        self.assertListEqual([info], parsed)
 | 
			
		||||
 | 
			
		||||
    def test_generator_of_more_than_one_resouurce(self):
 | 
			
		||||
        """Verify we handle multiple items in a generator correctly."""
 | 
			
		||||
        info_dicts = [self.resource_info(id=i) for i in range(10)]
 | 
			
		||||
        self.formatter.handle(crud.Resource(mock.Mock(), info, True)
 | 
			
		||||
                              for info in info_dicts)
 | 
			
		||||
        parsed = self.load_json(self.stripped_stdout())
 | 
			
		||||
        self.assertListEqual(info_dicts, parsed)
 | 
			
		||||
							
								
								
									
										228
									
								
								cratonclient/tests/unit/formatters/test_table_formatter.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								cratonclient/tests/unit/formatters/test_table_formatter.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,228 @@
 | 
			
		||||
# 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.
 | 
			
		||||
"""Tests for the pretty-table formatter."""
 | 
			
		||||
import mock
 | 
			
		||||
import prettytable
 | 
			
		||||
 | 
			
		||||
from cratonclient import crud
 | 
			
		||||
from cratonclient.formatters import table
 | 
			
		||||
from cratonclient.tests.unit.formatters import base
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestTableFormatter(base.FormatterTestCase):
 | 
			
		||||
    """Tests for cratonclient.formatters.table.Formatter."""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        """Prepare test case for tests."""
 | 
			
		||||
        super(TestTableFormatter, self).setUp()
 | 
			
		||||
        self.print_patcher = mock.patch('cratonclient.formatters.table.print')
 | 
			
		||||
        self.formatter = table.Formatter(mock.Mock())
 | 
			
		||||
 | 
			
		||||
    def test_initialization(self):
 | 
			
		||||
        """Verify we set up defaults for our PrettyTable formatter."""
 | 
			
		||||
        self.assertEqual([], self.formatter.fields)
 | 
			
		||||
        self.assertEqual({}, self.formatter.formatters)
 | 
			
		||||
        self.assertEqual(0, self.formatter.sortby_index)
 | 
			
		||||
        self.assertEqual(set([]), self.formatter.mixed_case_fields)
 | 
			
		||||
        self.assertEqual([], self.formatter.field_labels)
 | 
			
		||||
        self.assertEqual("Property", self.formatter.dict_property)
 | 
			
		||||
        self.assertEqual("Value", self.formatter.dict_value)
 | 
			
		||||
        self.assertEqual(0, self.formatter.wrap)
 | 
			
		||||
 | 
			
		||||
    # Case 0: "Everything" that isn't one of the special cases below
 | 
			
		||||
    def test_configure(self):
 | 
			
		||||
        """Verify we can configure our formatter.
 | 
			
		||||
 | 
			
		||||
        There are a few special pieces of logic. For the simpler cases, we can
 | 
			
		||||
        just exercise those branches here.
 | 
			
		||||
        """
 | 
			
		||||
        self.formatter.configure(
 | 
			
		||||
            mixed_case_fields=['Foo', 'Bar'],
 | 
			
		||||
            dict_property='Field',
 | 
			
		||||
            dict_value='Stored Value',
 | 
			
		||||
            wrap=72,
 | 
			
		||||
            # NOTE(sigmavirus24): This value isn't accurate for formatters
 | 
			
		||||
            formatters={'foo': 'bar'},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assertEqual({'Foo', 'Bar'}, self.formatter.mixed_case_fields)
 | 
			
		||||
        self.assertEqual('Field', self.formatter.dict_property)
 | 
			
		||||
        self.assertEqual('Stored Value', self.formatter.dict_value)
 | 
			
		||||
        self.assertEqual(72, self.formatter.wrap)
 | 
			
		||||
        self.assertDictEqual({'foo': 'bar'}, self.formatter.formatters)
 | 
			
		||||
 | 
			
		||||
        # Assert defaults remain unchanged
 | 
			
		||||
        self.assertEqual([], self.formatter.fields)
 | 
			
		||||
        self.assertEqual([], self.formatter.field_labels)
 | 
			
		||||
        self.assertEqual(0, self.formatter.sortby_index)
 | 
			
		||||
 | 
			
		||||
    # Case 1: Just fields
 | 
			
		||||
    def test_configure_fields_only(self):
 | 
			
		||||
        """Verify the logic for configuring fields."""
 | 
			
		||||
        self.formatter.configure(fields=['id', 'name'])
 | 
			
		||||
        self.assertListEqual(['id', 'name'], self.formatter.fields)
 | 
			
		||||
        self.assertListEqual(['id', 'name'], self.formatter.field_labels)
 | 
			
		||||
 | 
			
		||||
    # Case 2: fields + field_labels
 | 
			
		||||
    def test_configure_fields_and_field_labels(self):
 | 
			
		||||
        """Verify the behaviour for specifying both fields and field_labels.
 | 
			
		||||
 | 
			
		||||
        When we specify both arguments, we need to ensure they're the same
 | 
			
		||||
        length. This demonstrates that we can specify different lists of the
 | 
			
		||||
        same length and one won't override the other.
 | 
			
		||||
        """
 | 
			
		||||
        self.formatter.configure(fields=['id', 'name'],
 | 
			
		||||
                                 field_labels=['name', 'id'])
 | 
			
		||||
        self.assertListEqual(['id', 'name'], self.formatter.fields)
 | 
			
		||||
        self.assertListEqual(['name', 'id'], self.formatter.field_labels)
 | 
			
		||||
 | 
			
		||||
    # Case 3: fields + field_labels different length
 | 
			
		||||
    def test_configure_incongruent_fields_and_field_labels(self):
 | 
			
		||||
        """Verify we check the length of fields and field_labels."""
 | 
			
		||||
        self.assertRaises(
 | 
			
		||||
            ValueError,
 | 
			
		||||
            self.formatter.configure,
 | 
			
		||||
            fields=['id', 'name', 'extra'],
 | 
			
		||||
            field_labels=['id', 'name'],
 | 
			
		||||
        )
 | 
			
		||||
        self.assertRaises(
 | 
			
		||||
            ValueError,
 | 
			
		||||
            self.formatter.configure,
 | 
			
		||||
            fields=['id', 'name'],
 | 
			
		||||
            field_labels=['id', 'name', 'extra'],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Case 4: sortby_index is None
 | 
			
		||||
    def test_configure_null_sortby_index(self):
 | 
			
		||||
        """Verify we can configure sortby_index to be None.
 | 
			
		||||
 | 
			
		||||
        In this case, the user does not want the table rows sorted.
 | 
			
		||||
        """
 | 
			
		||||
        self.formatter.configure(sortby_index=None)
 | 
			
		||||
        self.assertIsNone(self.formatter.sortby_index)
 | 
			
		||||
 | 
			
		||||
    # Case 5: sortby_index is an integer
 | 
			
		||||
    def test_configure_sortby_index_non_negative_int(self):
 | 
			
		||||
        """Verify we can configure sortby_index with an int."""
 | 
			
		||||
        self.formatter.configure(
 | 
			
		||||
            fields=['id', 'name'],
 | 
			
		||||
            sortby_index=1,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(1, self.formatter.sortby_index)
 | 
			
		||||
 | 
			
		||||
    # Case 6: sortby_index is a string of digits
 | 
			
		||||
    def test_configure_sortby_index_int_str(self):
 | 
			
		||||
        """Verify we can configure sortby_index with a str.
 | 
			
		||||
 | 
			
		||||
        It makes sense to also allow for strings of integers. This test
 | 
			
		||||
        ensures that they come out as integers on the other side.
 | 
			
		||||
        """
 | 
			
		||||
        self.formatter.configure(
 | 
			
		||||
            fields=['id', 'name'],
 | 
			
		||||
            sortby_index='1',
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(1, self.formatter.sortby_index)
 | 
			
		||||
 | 
			
		||||
    # Case 7: sortby_index is negative
 | 
			
		||||
    def test_configure_sortby_index_negative_int(self):
 | 
			
		||||
        """Verify we cannot configure sortby_index with a negative value.
 | 
			
		||||
 | 
			
		||||
        This will verify that we can neither pass negative integers nor
 | 
			
		||||
        strings with negative integer values.
 | 
			
		||||
        """
 | 
			
		||||
        self.assertRaises(
 | 
			
		||||
            ValueError,
 | 
			
		||||
            self.formatter.configure,
 | 
			
		||||
            fields=['id', 'name'],
 | 
			
		||||
            sortby_index='-1',
 | 
			
		||||
        )
 | 
			
		||||
        self.assertRaises(
 | 
			
		||||
            ValueError,
 | 
			
		||||
            self.formatter.configure,
 | 
			
		||||
            fields=['id', 'name'],
 | 
			
		||||
            sortby_index='-1',
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Case 8: sortby_index exceeds length of self.field_labels
 | 
			
		||||
    def test_configure_sortby_index_too_large_int(self):
 | 
			
		||||
        """Verify we can not use an index larger than the labels."""
 | 
			
		||||
        self.assertRaises(
 | 
			
		||||
            ValueError,
 | 
			
		||||
            self.formatter.configure,
 | 
			
		||||
            fields=['id', 'name'],
 | 
			
		||||
            sortby_index=3,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_sortby_kwargs(self):
 | 
			
		||||
        """Verify sortby_kwargs relies on sortby_index."""
 | 
			
		||||
        self.formatter.field_labels = ['id', 'created_at']
 | 
			
		||||
        self.assertDictEqual({'sortby': 'id'}, self.formatter.sortby_kwargs())
 | 
			
		||||
 | 
			
		||||
        self.formatter.sortby_index = 1
 | 
			
		||||
        self.assertDictEqual({'sortby': 'created_at'},
 | 
			
		||||
                             self.formatter.sortby_kwargs())
 | 
			
		||||
 | 
			
		||||
        self.formatter.sortby_index = None
 | 
			
		||||
        self.assertDictEqual({}, self.formatter.sortby_kwargs())
 | 
			
		||||
 | 
			
		||||
    def test_build_table(self):
 | 
			
		||||
        """Verify that we build our table and auto-align it."""
 | 
			
		||||
        table = self.formatter.build_table(['id', 'created_at'])
 | 
			
		||||
        self.assertIsInstance(table, prettytable.PrettyTable)
 | 
			
		||||
        self.assertDictEqual({'created_at': 'l', 'id': 'l'}, table.align)
 | 
			
		||||
 | 
			
		||||
    def test_build_table_with_labels(self):
 | 
			
		||||
        """Verify we pass along our field labels to our table."""
 | 
			
		||||
        with mock.patch('prettytable.PrettyTable') as PrettyTable:
 | 
			
		||||
            self.formatter.build_table(['id', 'created_at'])
 | 
			
		||||
 | 
			
		||||
        PrettyTable.assert_called_once_with(['id', 'created_at'])
 | 
			
		||||
 | 
			
		||||
    def test_handle_instance(self):
 | 
			
		||||
        """Verify our handling of resource instances."""
 | 
			
		||||
        resource = crud.Resource(mock.Mock(), self.resource_info())
 | 
			
		||||
        self.print_ = self.print_patcher.start()
 | 
			
		||||
        mocktable = mock.Mock()
 | 
			
		||||
        mocktable.get_string.return_value = ''
 | 
			
		||||
        with mock.patch('prettytable.PrettyTable') as PrettyTable:
 | 
			
		||||
            PrettyTable.return_value = mocktable
 | 
			
		||||
            self.formatter.handle_instance(resource)
 | 
			
		||||
        self.print_patcher.stop()
 | 
			
		||||
 | 
			
		||||
        PrettyTable.assert_called_once_with(["Property", "Value"])
 | 
			
		||||
        self.assertListEqual([
 | 
			
		||||
            mock.call(['id', 1]),
 | 
			
		||||
            mock.call(['name', 'Test Resource']),
 | 
			
		||||
        ], mocktable.add_row.call_args_list)
 | 
			
		||||
        self.print_.assert_called_once_with('')
 | 
			
		||||
 | 
			
		||||
    def test_handle_generator(self):
 | 
			
		||||
        """Verify how we handle generators of instances."""
 | 
			
		||||
        info_list = [self.resource_info(id=i) for i in range(15)]
 | 
			
		||||
        self.print_ = self.print_patcher.start()
 | 
			
		||||
        mocktable = mock.Mock()
 | 
			
		||||
        mocktable.get_string.return_value = ''
 | 
			
		||||
        self.formatter.configure(fields=['id', 'Name'])
 | 
			
		||||
        with mock.patch('prettytable.PrettyTable') as PrettyTable:
 | 
			
		||||
            PrettyTable.return_value = mocktable
 | 
			
		||||
            self.formatter.handle_generator(crud.Resource(mock.Mock(), info)
 | 
			
		||||
                                            for info in info_list)
 | 
			
		||||
 | 
			
		||||
        PrettyTable.assert_called_once_with(['id', 'Name'])
 | 
			
		||||
        self.assertListEqual(
 | 
			
		||||
            [mock.call([i, 'Test Resource']) for i in range(15)],
 | 
			
		||||
            mocktable.add_row.call_args_list,
 | 
			
		||||
        )
 | 
			
		||||
        mocktable.get_string.assert_called_once_with(sortby='id')
 | 
			
		||||
        self.print_.assert_called_once_with('')
 | 
			
		||||
@@ -121,6 +121,10 @@ class TestCratonShell(base.TestCase):
 | 
			
		||||
                '--version', action='version',
 | 
			
		||||
                version=cratonclient.__version__,
 | 
			
		||||
            ),
 | 
			
		||||
            mock.call(
 | 
			
		||||
                '--format', default='default', choices=['default', 'json'],
 | 
			
		||||
                help=mock.ANY,
 | 
			
		||||
            ),
 | 
			
		||||
            mock.call(
 | 
			
		||||
                '--craton-url', default='',
 | 
			
		||||
                help='The base URL of the running Craton service. '
 | 
			
		||||
 
 | 
			
		||||
@@ -8,3 +8,4 @@ oslo.utils>=3.18.0 # Apache-2.0
 | 
			
		||||
pbr>=1.8 # Apache-2.0
 | 
			
		||||
requests!=2.12.2,!=2.13.0,>=2.10.0 # Apache-2.0
 | 
			
		||||
keystoneauth1>=2.18.0 # Apache-2.0
 | 
			
		||||
stevedore>=1.17.1  # Apache-2.0
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user