Retire Packaging Deb project repos

This commit is part of a series to retire the Packaging Deb
project. Step 2 is to remove all content from the project
repos, replacing it with a README notification where to find
ongoing work, and how to recover the repo if needed at some
future point (as in
https://docs.openstack.org/infra/manual/drivers.html#retiring-a-project).

Change-Id: I41a3e231882883484b2929706e5ea57616259784
This commit is contained in:
Tony Breeds
2017-09-12 15:57:02 -06:00
parent 772e16d6bb
commit e04606ee60
87 changed files with 14 additions and 8184 deletions

43
.gitignore vendored
View File

@@ -1,43 +0,0 @@
*.py[co]
# Packages
*.egg*
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
.venv
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
#Translations
*.mo
#Mr Developer
.mr.developer.cfg
#sample output
*.log
*.log.*
# pbr output
AUTHORS
ChangeLog
# Editors
*~
.*.swp
/.testrepository/
/cover/
.coverage.*

View File

@@ -1,4 +0,0 @@
[gerrit]
host=review.openstack.org
port=29418
project=openstack/cliff.git

View File

@@ -1,7 +0,0 @@
[DEFAULT]
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
${PYTHON:-python} -m subunit.run discover -t ./ ./cliff $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

View File

@@ -1,8 +0,0 @@
language: python
python:
- 2.7
- 3.2
- 3.3
- pypy
install: pip install -r test-requirements.txt
script: nosetests -d

View File

@@ -1,10 +0,0 @@
Changes to cliff should be submitted for review via the Gerrit tool,
following the workflow documented at:
http://docs.openstack.org/infra/manual/developers.html#development-workflow
Pull requests submitted through GitHub will be ignored.
Bugs should be filed on Launchpad, not GitHub:
https://bugs.launchpad.net/python-cliff

202
LICENSE
View File

@@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View File

@@ -1,6 +0,0 @@
include setup.py
recursive-include docs *.rst *.py *.html *.css *.js *.png *.txt
recursive-include demoapp *.py
recursive-include tests *.py
include tox.ini

View File

@@ -1,21 +0,0 @@
help:
@echo "release - package and upload a release"
@echo "sdist - package"
@echo "docs - generate HTML documentation"
@echo "clean - remove build artifacts"
release: docs
rm -rf dist build
python setup.py sdist upload
sdist: docs
python setup.py sdist
ls -l dist
clean:
rm -rf dist build *.egg-info
(cd doc && make clean)
.PHONY: docs
docs:
(cd doc && make clean html)

14
README Normal file
View File

@@ -0,0 +1,14 @@
This project is no longer maintained.
The contents of this repository are still available in the Git
source code management system. To see the contents of this
repository before it reached its end of life, please check out the
previous commit with "git checkout HEAD^1".
For ongoing work on maintaining OpenStack packages in the Debian
distribution, please see the Debian OpenStack packaging team at
https://wiki.debian.org/OpenStack/.
For any further questions, please email
openstack-dev@lists.openstack.org or join #openstack-dev on
Freenode.

View File

@@ -1,23 +0,0 @@
========================
Team and repository tags
========================
.. image:: http://governance.openstack.org/badges/cliff.svg
:target: http://governance.openstack.org/reference/tags/index.html
.. Change things from this point on
=======================================================
cliff -- Command Line Interface Formulation Framework
=======================================================
cliff is a framework for building command line programs. It uses
`setuptools entry points`_ to provide subcommands, output formatters, and
other extensions.
.. _setuptools entry points: http://setuptools.readthedocs.io/en/latest/pkg_resources.html#convenience-api
* Free software: Apache license
* Documentation: http://docs.openstack.org/developer/cliff
* Source: http://git.openstack.org/cgit/openstack/cliff
* Bugs: https://bugs.launchpad.net/python-cliff

View File

View File

@@ -1,30 +0,0 @@
# 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.
"""Special argparse module that allows to bypass abbrev mode."""
from __future__ import absolute_import
from argparse import * # noqa
import sys
if sys.version_info < (3, 5):
class ArgumentParser(ArgumentParser): # noqa
def __init__(self, *args, **kwargs):
self.allow_abbrev = kwargs.pop("allow_abbrev", True)
super(ArgumentParser, self).__init__(*args, **kwargs)
def _get_option_tuples(self, option_string):
if self.allow_abbrev:
return super(ArgumentParser, self)._get_option_tuples(
option_string)
return ()

View File

@@ -1,423 +0,0 @@
# 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.
"""Application base class.
"""
import codecs
import inspect
import locale
import logging
import logging.handlers
import os
import six
import sys
from cliff import _argparse
from . import complete
from . import help
from . import utils
logging.getLogger('cliff').addHandler(logging.NullHandler())
class App(object):
"""Application base class.
:param description: one-liner explaining the program purpose
:paramtype description: str
:param version: application version number
:paramtype version: str
:param command_manager: plugin loader
:paramtype command_manager: cliff.commandmanager.CommandManager
:param stdin: Standard input stream
:paramtype stdin: readable I/O stream
:param stdout: Standard output stream
:paramtype stdout: writable I/O stream
:param stderr: Standard error output stream
:paramtype stderr: writable I/O stream
:param interactive_app_factory: callable to create an
interactive application
:paramtype interactive_app_factory: cliff.interactive.InteractiveApp
:param deferred_help: True - Allow subcommands to accept --help with
allowing to defer help print after initialize_app
:paramtype deferred_help: bool
"""
NAME = os.path.splitext(os.path.basename(sys.argv[0]))[0]
LOG = logging.getLogger(NAME)
CONSOLE_MESSAGE_FORMAT = '%(message)s'
LOG_FILE_MESSAGE_FORMAT = \
'[%(asctime)s] %(levelname)-8s %(name)s %(message)s'
DEFAULT_VERBOSE_LEVEL = 1
DEFAULT_OUTPUT_ENCODING = 'utf-8'
def __init__(self, description, version, command_manager,
stdin=None, stdout=None, stderr=None,
interactive_app_factory=None,
deferred_help=False):
"""Initialize the application.
"""
self.command_manager = command_manager
self.command_manager.add_command('help', help.HelpCommand)
self.command_manager.add_command('complete', complete.CompleteCommand)
self._set_streams(stdin, stdout, stderr)
self.interactive_app_factory = interactive_app_factory
self.deferred_help = deferred_help
self.parser = self.build_option_parser(description, version)
self.interactive_mode = False
self.interpreter = None
def _set_streams(self, stdin, stdout, stderr):
try:
locale.setlocale(locale.LC_ALL, '')
except locale.Error:
pass
# Unicode must be encoded/decoded for text I/O streams, the
# correct encoding for the stream must be selected and it must
# be capable of handling the set of characters in the stream
# or Python will raise a codec error. The correct codec is
# selected based on the locale. Python2 uses the locales
# encoding but only when the I/O stream is attached to a
# terminal (TTY) otherwise it uses the default ASCII
# encoding. The effect is internationalized text written to
# the terminal works as expected but if command line output is
# redirected (file or pipe) the ASCII codec is used and the
# program aborts with a codec error.
#
# The default I/O streams stdin, stdout and stderr can be
# wrapped in a codec based on the locale thus assuring the
# users desired encoding is always used no matter the I/O
# destination. Python3 does this by default.
#
# If the caller supplies an I/O stream we use it unmodified on
# the assumption the caller has taken all responsibility for
# the stream. But with Python2 if the caller allows us to
# default the I/O streams to sys.stdin, sys.stdout and
# sys.stderr we apply the locales encoding just as Python3
# would do. We also check to make sure the main Python program
# has not already already wrapped sys.stdin, sys.stdout and
# sys.stderr as this is a common recommendation.
if six.PY2:
encoding = locale.getpreferredencoding()
if encoding:
if not (stdin or isinstance(sys.stdin, codecs.StreamReader)):
stdin = codecs.getreader(encoding)(sys.stdin)
if not (stdout or isinstance(sys.stdout, codecs.StreamWriter)):
stdout = codecs.getwriter(encoding)(sys.stdout)
if not (stderr or isinstance(sys.stderr, codecs.StreamWriter)):
stderr = codecs.getwriter(encoding)(sys.stderr)
self.stdin = stdin or sys.stdin
self.stdout = stdout or sys.stdout
self.stderr = stderr or sys.stderr
def build_option_parser(self, description, version,
argparse_kwargs=None):
"""Return an argparse option parser for this application.
Subclasses may override this method to extend
the parser with more global options.
:param description: full description of the application
:paramtype description: str
:param version: version number for the application
:paramtype version: str
:param argparse_kwargs: extra keyword argument passed to the
ArgumentParser constructor
:paramtype extra_kwargs: dict
"""
argparse_kwargs = argparse_kwargs or {}
parser = _argparse.ArgumentParser(
description=description,
add_help=False,
**argparse_kwargs
)
parser.add_argument(
'--version',
action='version',
version='%(prog)s {0}'.format(version),
)
verbose_group = parser.add_mutually_exclusive_group()
verbose_group.add_argument(
'-v', '--verbose',
action='count',
dest='verbose_level',
default=self.DEFAULT_VERBOSE_LEVEL,
help='Increase verbosity of output. Can be repeated.',
)
verbose_group.add_argument(
'-q', '--quiet',
action='store_const',
dest='verbose_level',
const=0,
help='Suppress output except warnings and errors.',
)
parser.add_argument(
'--log-file',
action='store',
default=None,
help='Specify a file to log output. Disabled by default.',
)
if self.deferred_help:
parser.add_argument(
'-h', '--help',
dest='deferred_help',
action='store_true',
help="Show help message and exit.",
)
else:
parser.add_argument(
'-h', '--help',
action=help.HelpAction,
nargs=0,
default=self, # tricky
help="Show help message and exit.",
)
parser.add_argument(
'--debug',
default=False,
action='store_true',
help='Show tracebacks on errors.',
)
return parser
def configure_logging(self):
"""Create logging handlers for any log output.
"""
root_logger = logging.getLogger('')
root_logger.setLevel(logging.DEBUG)
# Set up logging to a file
if self.options.log_file:
file_handler = logging.FileHandler(
filename=self.options.log_file,
)
formatter = logging.Formatter(self.LOG_FILE_MESSAGE_FORMAT)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
# Always send higher-level messages to the console via stderr
console = logging.StreamHandler(self.stderr)
console_level = {0: logging.WARNING,
1: logging.INFO,
2: logging.DEBUG,
}.get(self.options.verbose_level, logging.DEBUG)
console.setLevel(console_level)
formatter = logging.Formatter(self.CONSOLE_MESSAGE_FORMAT)
console.setFormatter(formatter)
root_logger.addHandler(console)
return
def print_help_if_requested(self):
"""Print help and exits if deferred help is enabled and requested.
'--help' shows the help message and exits:
* without calling initialize_app if not self.deferred_help (default),
* after initialize_app call if self.deferred_help,
* during initialize_app call if self.deferred_help and subclass calls
explicitly this method in initialize_app.
"""
if self.deferred_help and self.options.deferred_help:
action = help.HelpAction(None, None, default=self)
action(self.parser, self.options, None, None)
def run(self, argv):
"""Equivalent to the main program for the application.
:param argv: input arguments and options
:paramtype argv: list of str
"""
try:
self.options, remainder = self.parser.parse_known_args(argv)
self.configure_logging()
self.interactive_mode = not remainder
if self.deferred_help and self.options.deferred_help and remainder:
# When help is requested and `remainder` has any values disable
# `deferred_help` and instead allow the help subcommand to
# handle the request during run_subcommand(). This turns
# "app foo bar --help" into "app help foo bar". However, when
# `remainder` is empty use print_help_if_requested() to allow
# for an early exit.
# Disabling `deferred_help` here also ensures that
# print_help_if_requested will not fire if called by a subclass
# during its initialize_app().
self.options.deferred_help = False
remainder.insert(0, "help")
self.initialize_app(remainder)
self.print_help_if_requested()
except Exception as err:
if hasattr(self, 'options'):
debug = self.options.debug
else:
debug = True
if debug:
self.LOG.exception(err)
raise
else:
self.LOG.error(err)
return 1
result = 1
if self.interactive_mode:
result = self.interact()
else:
result = self.run_subcommand(remainder)
return result
# FIXME(dhellmann): Consider moving these command handling methods
# to a separate class.
def initialize_app(self, argv):
"""Hook for subclasses to take global initialization action
after the arguments are parsed but before a command is run.
Invoked only once, even in interactive mode.
:param argv: List of arguments, including the subcommand to run.
Empty for interactive mode.
"""
return
def prepare_to_run_command(self, cmd):
"""Perform any preliminary work needed to run a command.
:param cmd: command processor being invoked
:paramtype cmd: cliff.command.Command
"""
return
def clean_up(self, cmd, result, err):
"""Hook run after a command is done to shutdown the app.
:param cmd: command processor being invoked
:paramtype cmd: cliff.command.Command
:param result: return value of cmd
:paramtype result: int
:param err: exception or None
:paramtype err: Exception
"""
return
def interact(self):
# Defer importing .interactive as cmd2 is a slow import
from .interactive import InteractiveApp
if self.interactive_app_factory is None:
self.interactive_app_factory = InteractiveApp
self.interpreter = self.interactive_app_factory(self,
self.command_manager,
self.stdin,
self.stdout,
)
self.interpreter.cmdloop()
return 0
def get_fuzzy_matches(self, cmd):
"""return fuzzy matches of unknown command
"""
sep = '_'
if self.command_manager.convert_underscores:
sep = ' '
all_cmds = [k[0] for k in self.command_manager]
dist = []
for candidate in sorted(all_cmds):
prefix = candidate.split(sep)[0]
# Give prefix match a very good score
if candidate.startswith(cmd):
dist.append((0, candidate))
continue
# Levenshtein distance
dist.append((utils.damerau_levenshtein(cmd, prefix, utils.COST)+1,
candidate))
matches = []
match_distance = 0
for distance, candidate in sorted(dist):
if distance > match_distance:
if match_distance:
# we copied all items with minimum distance, we are done
break
# we copied all items with distance=0,
# now we match all candidates at the minimum distance
match_distance = distance
matches.append(candidate)
return matches
def run_subcommand(self, argv):
try:
subcommand = self.command_manager.find_command(argv)
except ValueError as err:
# If there was no exact match, try to find a fuzzy match
the_cmd = argv[0]
fuzzy_matches = self.get_fuzzy_matches(the_cmd)
if fuzzy_matches:
article = 'a'
if self.NAME[0] in 'aeiou':
article = 'an'
self.stdout.write('%s: \'%s\' is not %s %s command. '
'See \'%s --help\'.\n'
% (self.NAME, ' '.join(argv), article,
self.NAME, self.NAME))
self.stdout.write('Did you mean one of these?\n')
for match in fuzzy_matches:
self.stdout.write(' %s\n' % match)
else:
if self.options.debug:
raise
else:
self.LOG.error(err)
return 2
cmd_factory, cmd_name, sub_argv = subcommand
kwargs = {}
if 'cmd_name' in inspect.getargspec(cmd_factory.__init__).args:
kwargs['cmd_name'] = cmd_name
cmd = cmd_factory(self, self.options, **kwargs)
err = None
result = 1
try:
self.prepare_to_run_command(cmd)
full_name = (cmd_name
if self.interactive_mode
else ' '.join([self.NAME, cmd_name])
)
cmd_parser = cmd.get_parser(full_name)
parsed_args = cmd_parser.parse_args(sub_argv)
result = cmd.run(parsed_args)
except Exception as err:
if self.options.debug:
self.LOG.exception(err)
else:
self.LOG.error(err)
try:
self.clean_up(cmd, result, err)
except Exception as err2:
if self.options.debug:
self.LOG.exception(err2)
else:
self.LOG.error('Could not clean up: %s', err2)
if self.options.debug:
raise
else:
try:
self.clean_up(cmd, result, None)
except Exception as err3:
if self.options.debug:
self.LOG.exception(err3)
else:
self.LOG.error('Could not clean up: %s', err3)
return result

View File

@@ -1,40 +0,0 @@
# 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.
"""Formattable column tools.
"""
import abc
import six
@six.add_metaclass(abc.ABCMeta)
class FormattableColumn(object):
def __init__(self, value):
self._value = value
@abc.abstractmethod
def human_readable(self):
"""Return a basic human readable version of the data.
"""
def machine_readable(self):
"""Return a raw data structure using only Python built-in types.
It must be possible to serialize the return value directly
using a formatter like JSON, and it will be up to the
formatter plugin to decide how to make that transformation.
"""
return self._value

View File

@@ -1,186 +0,0 @@
# 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 abc
import inspect
import six
from stevedore import extension
from cliff import _argparse
@six.add_metaclass(abc.ABCMeta)
class Command(object):
"""Base class for command plugins.
When the command is instantiated, it loads extensions from a
namespace based on the parent application namespace and the
command name::
app.namespace + '.' + cmd_name.replace(' ', '_')
:param app: Application instance invoking the command.
:paramtype app: cliff.app.App
"""
deprecated = False
_description = ''
_epilog = None
def __init__(self, app, app_args, cmd_name=None):
self.app = app
self.app_args = app_args
self.cmd_name = cmd_name
self._load_hooks()
def _load_hooks(self):
# Look for command extensions
if self.app and self.cmd_name:
namespace = '{}.{}'.format(
self.app.command_manager.namespace,
self.cmd_name.replace(' ', '_')
)
self._hooks = extension.ExtensionManager(
namespace=namespace,
invoke_on_load=True,
invoke_kwds={
'command': self,
},
)
else:
# Setting _hooks to an empty list allows iteration without
# checking if there are hooks every time.
self._hooks = []
return
def get_description(self):
"""Return the command description.
The default is to use the first line of the class' docstring
as the description. Set the ``_description`` class attribute
to a one-line description of a command to use a different
value. This is useful for enabling translations, for example,
with ``_description`` set to a string wrapped with a gettext
translation marker.
"""
# NOTE(dhellmann): We need the trailing "or ''" because under
# Python 2.7 the default for the docstring is None instead of
# an empty string, and we always want this method to return a
# string.
desc = self._description or inspect.getdoc(self.__class__) or ''
# The base class command description isn't useful for any
# real commands, so ignore that value.
if desc == inspect.getdoc(Command):
desc = ''
return desc
def get_epilog(self):
"""Return the command epilog."""
hook_epilogs = filter(
None,
(h.obj.get_epilog() for h in self._hooks),
)
if hook_epilogs:
# combine them, replacing a None in self._epilog with an
# empty string
parts = [self._epilog or '']
parts.extend(hook_epilogs)
return '\n\n'.join(parts)
return self._epilog
def get_parser(self, prog_name):
"""Return an :class:`argparse.ArgumentParser`.
"""
parser = _argparse.ArgumentParser(
description=self.get_description(),
epilog=self.get_epilog(),
prog=prog_name,
formatter_class=_SmartHelpFormatter,
)
for hook in self._hooks:
hook.obj.get_parser(parser)
return parser
@abc.abstractmethod
def take_action(self, parsed_args):
"""Override to do something useful.
The returned value will be returned by the program.
"""
def run(self, parsed_args):
"""Invoked by the application when the command is run.
Developers implementing commands should override
:meth:`take_action`.
Developers creating new command base classes (such as
:class:`Lister` and :class:`ShowOne`) should override this
method to wrap :meth:`take_action`.
Return the value returned by :meth:`take_action` or 0.
"""
self._run_before_hooks(parsed_args)
return_code = self.take_action(parsed_args) or 0
self._run_after_hooks(parsed_args, return_code)
return return_code
def _run_before_hooks(self, parsed_args):
"""Calls before() method of the hooks.
This method is intended to be called from the run() method before
take_action() is called.
This method should only be overriden by developers creating new
command base classes and only if it is necessary to have different
hook processing behavior.
"""
for hook in self._hooks:
hook.obj.before(parsed_args)
def _run_after_hooks(self, parsed_args, return_code):
"""Calls after() method of the hooks.
This method is intended to be called from the run() method after
take_action() is called.
This method should only be overriden by developers creating new
command base classes and only if it is necessary to have different
hook processing behavior.
"""
for hook in self._hooks:
hook.obj.after(parsed_args, return_code)
class _SmartHelpFormatter(_argparse.HelpFormatter):
"""Smart help formatter to output raw help message if help contain \n.
Some command help messages maybe have multiple line content, the built-in
argparse.HelpFormatter wrap and split the content according to width, and
ignore \n in the raw help message, it merge multiple line content in one
line to output, that looks messy. SmartHelpFormatter keep the raw help
message format if it contain \n, and wrap long line like HelpFormatter
behavior.
"""
def _split_lines(self, text, width):
lines = text.splitlines() if '\n' in text else [text]
wrap_lines = []
for each_line in lines:
wrap_lines.extend(
super(_SmartHelpFormatter, self)._split_lines(each_line, width)
)
return wrap_lines

View File

@@ -1,104 +0,0 @@
# 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.
"""Discover and lookup command plugins.
"""
import inspect
import logging
import pkg_resources
LOG = logging.getLogger(__name__)
class EntryPointWrapper(object):
"""Wrap up a command class already imported to make it look like a plugin.
"""
def __init__(self, name, command_class):
self.name = name
self.command_class = command_class
def load(self, require=False):
return self.command_class
class CommandManager(object):
"""Discovers commands and handles lookup based on argv data.
:param namespace: String containing the setuptools entrypoint namespace
for the plugins to be loaded. For example,
``'cliff.formatter.list'``.
:param convert_underscores: Whether cliff should convert underscores to
spaces in entry_point commands.
"""
def __init__(self, namespace, convert_underscores=True):
self.commands = {}
self.namespace = namespace
self.convert_underscores = convert_underscores
self._load_commands()
def _load_commands(self):
# NOTE(jamielennox): kept for compatibility.
self.load_commands(self.namespace)
def load_commands(self, namespace):
"""Load all the commands from an entrypoint"""
for ep in pkg_resources.iter_entry_points(namespace):
LOG.debug('found command %r', ep.name)
cmd_name = (ep.name.replace('_', ' ')
if self.convert_underscores
else ep.name)
self.commands[cmd_name] = ep
return
def __iter__(self):
return iter(self.commands.items())
def add_command(self, name, command_class):
self.commands[name] = EntryPointWrapper(name, command_class)
def find_command(self, argv):
"""Given an argument list, find a command and
return the processor and any remaining arguments.
"""
start = self._get_last_possible_command_index(argv)
for i in range(start, 0, -1):
name = ' '.join(argv[:i])
search_args = argv[i:]
if name in self.commands:
cmd_ep = self.commands[name]
if hasattr(cmd_ep, 'resolve'):
cmd_factory = cmd_ep.resolve()
else:
# NOTE(dhellmann): Some fake classes don't take
# require as an argument. Yay?
arg_spec = inspect.getargspec(cmd_ep.load)
if 'require' in arg_spec[0]:
cmd_factory = cmd_ep.load(require=False)
else:
cmd_factory = cmd_ep.load()
return (cmd_factory, name, search_args)
else:
raise ValueError('Unknown command %r' %
(argv,))
def _get_last_possible_command_index(self, argv):
"""Returns the index after the last argument
in argv that can be a command word
"""
for i, arg in enumerate(argv):
if arg.startswith('-'):
return i
return len(argv)

View File

@@ -1,221 +0,0 @@
# 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.
"""Bash completion for the CLI.
"""
import logging
import six
import stevedore
from cliff import command
class CompleteDictionary:
"""dictionary for bash completion
"""
def __init__(self):
self._dictionary = {}
def add_command(self, command, actions):
optstr = ' '.join(opt for action in actions
for opt in action.option_strings)
dicto = self._dictionary
last_cmd = command[-1]
for subcmd in command[:-1]:
subdata = dicto.get(subcmd)
# If there is a string in corresponding dictionary, it means the
# verb used for the command exists already.
# For example, {'cmd': 'action'}, and we add the command
# 'cmd_other'. We want the result to be
# {'cmd': 'action other', 'cmd_other': 'sub_action'}
if isinstance(subdata, six.string_types):
subdata += ' ' + last_cmd
dicto[subcmd] = subdata
last_cmd = subcmd + '_' + last_cmd
else:
dicto = dicto.setdefault(subcmd, {})
dicto[last_cmd] = optstr
def get_commands(self):
return ' '.join(k for k in sorted(self._dictionary.keys()))
def _get_data_recurse(self, dictionary, path):
ray = []
keys = sorted(dictionary.keys())
for cmd in keys:
name = path + "_" + cmd if path else cmd
value = dictionary[cmd]
if isinstance(value, six.string_types):
ray.append((name, value))
else:
cmdlist = ' '.join(sorted(value.keys()))
ray.append((name, cmdlist))
ray += self._get_data_recurse(value, name)
return ray
def get_data(self):
return sorted(self._get_data_recurse(self._dictionary, ""))
class CompleteShellBase(object):
"""base class for bash completion generation
"""
def __init__(self, name, output):
self.name = str(name)
self.output = output
def write(self, cmdo, data):
self.output.write(self.get_header())
self.output.write(" cmds='{0}'\n".format(cmdo))
for datum in data:
datum = (datum[0].replace('-', '_'), datum[1])
self.output.write(' cmds_{0}=\'{1}\'\n'.format(*datum))
self.output.write(self.get_trailer())
@property
def escaped_name(self):
return self.name.replace('-', '_')
class CompleteNoCode(CompleteShellBase):
"""completion with no code
"""
def __init__(self, name, output):
super(CompleteNoCode, self).__init__(name, output)
def get_header(self):
return ''
def get_trailer(self):
return ''
class CompleteBash(CompleteShellBase):
"""completion for bash
"""
def __init__(self, name, output):
super(CompleteBash, self).__init__(name, output)
def get_header(self):
return ('_' + self.escaped_name + """()
{
local cur prev words
COMPREPLY=()
_get_comp_words_by_ref -n : cur prev words
# Command data:
""")
def get_trailer(self):
return ("""
dash=-
underscore=_
cmd=""
words[0]=""
completed="${cmds}"
for var in "${words[@]:1}"
do
if [[ ${var} == -* ]] ; then
break
fi
if [ -z "${cmd}" ] ; then
proposed="${var}"
else
proposed="${cmd}_${var}"
fi
local i="cmds_${proposed}"
i=${i//$dash/$underscore}
local comp="${!i}"
if [ -z "${comp}" ] ; then
break
fi
if [[ ${comp} == -* ]] ; then
if [[ ${cur} != -* ]] ; then
completed=""
break
fi
fi
cmd="${proposed}"
completed="${comp}"
done
if [ -z "${completed}" ] ; then
COMPREPLY=( $( compgen -f -- "$cur" ) $( compgen -d -- "$cur" ) )
else
COMPREPLY=( $(compgen -W "${completed}" -- ${cur}) )
fi
return 0
}
complete -F _""" + self.escaped_name + ' ' + self.name + '\n')
class CompleteCommand(command.Command):
"""print bash completion command
"""
log = logging.getLogger(__name__ + '.CompleteCommand')
def __init__(self, app, app_args, cmd_name=None):
super(CompleteCommand, self).__init__(app, app_args, cmd_name)
self._formatters = stevedore.ExtensionManager(
namespace='cliff.formatter.completion',
)
def get_parser(self, prog_name):
parser = super(CompleteCommand, self).get_parser(prog_name)
parser.add_argument(
"--name",
default=None,
metavar='<command_name>',
help="Command name to support with command completion"
)
parser.add_argument(
"--shell",
default='bash',
metavar='<shell>',
choices=sorted(self._formatters.names()),
help="Shell being used. Use none for data only (default: bash)"
)
return parser
def get_actions(self, command):
the_cmd = self.app.command_manager.find_command(command)
cmd_factory, cmd_name, search_args = the_cmd
cmd = cmd_factory(self.app, search_args)
if self.app.interactive_mode:
full_name = (cmd_name)
else:
full_name = (' '.join([self.app.NAME, cmd_name]))
cmd_parser = cmd.get_parser(full_name)
return cmd_parser._get_optional_actions()
def take_action(self, parsed_args):
self.log.debug('take_action(%s)' % parsed_args)
name = parsed_args.name or self.app.NAME
try:
shell_factory = self._formatters[parsed_args.shell].plugin
except KeyError:
raise RuntimeError('Unknown shell syntax %r' % parsed_args.shell)
shell = shell_factory(name, self.app.stdout)
dicto = CompleteDictionary()
for cmd in self.app.command_manager:
command = cmd[0].split()
dicto.add_command(command, self.get_actions(command))
shell.write(dicto.get_commands(), dicto.get_data())
return 0

View File

@@ -1,120 +0,0 @@
# 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.
"""Application base class for displaying data.
"""
import abc
from itertools import compress
import six
import stevedore
from . import command
@six.add_metaclass(abc.ABCMeta)
class DisplayCommandBase(command.Command):
"""Command base class for displaying data about a single object.
"""
def __init__(self, app, app_args, cmd_name=None):
super(DisplayCommandBase, self).__init__(app, app_args,
cmd_name=cmd_name)
self._formatter_plugins = self._load_formatter_plugins()
@abc.abstractproperty
def formatter_namespace(self):
"String specifying the namespace to use for loading formatter plugins."
@abc.abstractproperty
def formatter_default(self):
"String specifying the name of the default formatter."
def _load_formatter_plugins(self):
# Here so tests can override
return stevedore.ExtensionManager(
self.formatter_namespace,
invoke_on_load=True,
)
def get_parser(self, prog_name):
parser = super(DisplayCommandBase, self).get_parser(prog_name)
formatter_group = parser.add_argument_group(
title='output formatters',
description='output formatter options',
)
formatter_choices = sorted(self._formatter_plugins.names())
formatter_default = self.formatter_default
if formatter_default not in formatter_choices:
formatter_default = formatter_choices[0]
formatter_group.add_argument(
'-f', '--format',
dest='formatter',
action='store',
choices=formatter_choices,
default=formatter_default,
help='the output format, defaults to %s' % formatter_default,
)
formatter_group.add_argument(
'-c', '--column',
action='append',
default=[],
dest='columns',
metavar='COLUMN',
help='specify the column(s) to include, can be repeated',
)
for formatter in self._formatter_plugins:
formatter.obj.add_argument_group(parser)
return parser
@abc.abstractmethod
def produce_output(self, parsed_args, column_names, data):
"""Use the formatter to generate the output.
:param parsed_args: argparse.Namespace instance with argument values
:param column_names: sequence of strings containing names
of output columns
:param data: iterable with values matching the column names
"""
def _generate_columns_and_selector(self, parsed_args, column_names):
"""Generate included columns and selector according to parsed args.
:param parsed_args: argparse.Namespace instance with argument values
:param column_names: sequence of strings containing names
of output columns
"""
if not parsed_args.columns:
columns_to_include = column_names
selector = None
else:
columns_to_include = [c for c in column_names
if c in parsed_args.columns]
if not columns_to_include:
raise ValueError('No recognized column names in %s' %
str(parsed_args.columns))
# Set up argument to compress()
selector = [(c in columns_to_include)
for c in column_names]
return columns_to_include, selector
def run(self, parsed_args):
self._run_before_hooks(parsed_args)
self.formatter = self._formatter_plugins[parsed_args.formatter].obj
column_names, data = self.take_action(parsed_args)
self._run_after_hooks(parsed_args, (column_names, data))
self.produce_output(parsed_args, column_names, data)
return 0
@staticmethod
def _compress_iterable(iterable, selectors):
return compress(iterable, selectors)

View File

@@ -1,75 +0,0 @@
# 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 classes for formatters.
"""
import abc
import six
@six.add_metaclass(abc.ABCMeta)
class Formatter(object):
@abc.abstractmethod
def add_argument_group(self, parser):
"""Add any options to the argument parser.
Should use our own argument group.
"""
@six.add_metaclass(abc.ABCMeta)
class ListFormatter(Formatter):
"""Base class for formatters that know how to deal with multiple objects.
"""
@abc.abstractmethod
def emit_list(self, column_names, data, stdout, parsed_args):
"""Format and print the list from the iterable data source.
Data values can be primitive types like ints and strings, or
can be an instance of a :class:`FormattableColumn` for
situations where the value is complex, and may need to be
handled differently for human readable output vs. machine
readable output.
:param column_names: names of the columns
:param data: iterable data source, one tuple per object
with values in order of column names
:param stdout: output stream where data should be written
:param parsed_args: argparse namespace from our local options
"""
@six.add_metaclass(abc.ABCMeta)
class SingleFormatter(Formatter):
"""Base class for formatters that work with single objects.
"""
@abc.abstractmethod
def emit_one(self, column_names, data, stdout, parsed_args):
"""Format and print the values associated with the single object.
Data values can be primitive types like ints and strings, or
can be an instance of a :class:`FormattableColumn` for
situations where the value is complex, and may need to be
handled differently for human readable output vs. machine
readable output.
:param column_names: names of the columns
:param data: iterable data source with values in order of column names
:param stdout: output stream where data should be written
:param parsed_args: argparse namespace from our local options
"""

View File

@@ -1,63 +0,0 @@
# 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.
"""Output formatters using csv format.
"""
import os
import sys
from .base import ListFormatter
from cliff import columns
import six
if sys.version_info[0] == 3:
import csv
else:
import unicodecsv as csv
class CSVLister(ListFormatter):
QUOTE_MODES = {
'all': csv.QUOTE_ALL,
'minimal': csv.QUOTE_MINIMAL,
'nonnumeric': csv.QUOTE_NONNUMERIC,
'none': csv.QUOTE_NONE,
}
def add_argument_group(self, parser):
group = parser.add_argument_group('CSV Formatter')
group.add_argument(
'--quote',
choices=sorted(self.QUOTE_MODES.keys()),
dest='quote_mode',
default='nonnumeric',
help='when to include quotes, defaults to nonnumeric',
)
def emit_list(self, column_names, data, stdout, parsed_args):
writer = csv.writer(stdout,
quoting=self.QUOTE_MODES[parsed_args.quote_mode],
lineterminator=os.linesep,
escapechar='\\',
)
writer.writerow(column_names)
for row in data:
writer.writerow(
[(six.text_type(c.machine_readable())
if isinstance(c, columns.FormattableColumn)
else c)
for c in row]
)
return

View File

@@ -1,54 +0,0 @@
# 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.
"""Output formatters for JSON.
"""
import json
from . import base
from cliff import columns
class JSONFormatter(base.ListFormatter, base.SingleFormatter):
def add_argument_group(self, parser):
group = parser.add_argument_group(title='json formatter')
group.add_argument(
'--noindent',
action='store_true',
dest='noindent',
help='whether to disable indenting the JSON'
)
def emit_list(self, column_names, data, stdout, parsed_args):
items = []
for item in data:
items.append(
{n: (i.machine_readable()
if isinstance(i, columns.FormattableColumn)
else i)
for n, i in zip(column_names, item)}
)
indent = None if parsed_args.noindent else 2
json.dump(items, stdout, indent=indent)
stdout.write('\n')
def emit_one(self, column_names, data, stdout, parsed_args):
one = {
n: (i.machine_readable()
if isinstance(i, columns.FormattableColumn)
else i)
for n, i in zip(column_names, data)
}
indent = None if parsed_args.noindent else 2
json.dump(one, stdout, indent=indent)

View File

@@ -1,65 +0,0 @@
# 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.
"""Output formatters using shell syntax.
"""
from . import base
from cliff import columns
import argparse
import six
class ShellFormatter(base.SingleFormatter):
def add_argument_group(self, parser):
group = parser.add_argument_group(
title='shell formatter',
description='a format a UNIX shell can parse (variable="value")',
)
group.add_argument(
'--variable',
action='append',
default=[],
dest='variables',
metavar='VARIABLE',
help=argparse.SUPPRESS,
)
group.add_argument(
'--prefix',
action='store',
default='',
dest='prefix',
help='add a prefix to all variable names',
)
def emit_one(self, column_names, data, stdout, parsed_args):
variable_names = [c.lower().replace(' ', '_')
for c in column_names
]
desired_columns = parsed_args.variables
for name, value in zip(variable_names, data):
if name in desired_columns or not desired_columns:
value = (six.text_type(value.machine_readable())
if isinstance(value, columns.FormattableColumn)
else value)
if isinstance(value, six.string_types):
value = value.replace('"', '\\"')
if isinstance(name, six.string_types):
# Colons and dashes may appear as a resource property but
# are invalid to use in a shell, replace them with an
# underscore.
name = name.replace(':', '_')
name = name.replace('-', '_')
stdout.write('%s%s="%s"\n' % (parsed_args.prefix, name, value))
return

View File

@@ -1,217 +0,0 @@
# 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.
"""Output formatters using prettytable.
"""
import prettytable
import six
import os
from cliff import utils
from . import base
from cliff import columns
def _format_row(row):
new_row = []
for r in row:
if isinstance(r, columns.FormattableColumn):
r = r.human_readable()
if isinstance(r, six.string_types):
r = r.replace('\r\n', '\n').replace('\r', ' ')
new_row.append(r)
return new_row
class TableFormatter(base.ListFormatter, base.SingleFormatter):
ALIGNMENTS = {
int: 'r',
str: 'l',
float: 'r',
}
try:
ALIGNMENTS[unicode] = 'l'
except NameError:
pass
def add_argument_group(self, parser):
group = parser.add_argument_group('table formatter')
group.add_argument(
'--max-width',
metavar='<integer>',
default=int(os.environ.get('CLIFF_MAX_TERM_WIDTH', 0)),
type=int,
help=('Maximum display width, <1 to disable. You can also '
'use the CLIFF_MAX_TERM_WIDTH environment variable, '
'but the parameter takes precedence.'),
)
group.add_argument(
'--fit-width',
action='store_true',
default=bool(int(os.environ.get('CLIFF_FIT_WIDTH', 0))),
help=('Fit the table to the display width. '
'Implied if --max-width greater than 0. '
'Set the environment variable CLIFF_FIT_WIDTH=1 '
'to always enable'),
)
group.add_argument(
'--print-empty',
action='store_true',
help='Print empty table if there is no data to show.',
)
def add_rows(self, table, column_names, data):
# Figure out the types of the columns in the
# first row and set the alignment of the
# output accordingly.
data_iter = iter(data)
try:
first_row = next(data_iter)
except StopIteration:
pass
else:
for value, name in zip(first_row, column_names):
alignment = self.ALIGNMENTS.get(type(value), 'l')
table.align[name] = alignment
# Now iterate over the data and add the rows.
table.add_row(_format_row(first_row))
for row in data_iter:
table.add_row(_format_row(row))
def emit_list(self, column_names, data, stdout, parsed_args):
x = prettytable.PrettyTable(
column_names,
print_empty=parsed_args.print_empty,
)
x.padding_width = 1
# Add rows if data is provided
if data:
self.add_rows(x, column_names, data)
# Choose a reasonable min_width to better handle many columns on a
# narrow console. The table will overflow the console width in
# preference to wrapping columns smaller than 8 characters.
min_width = 8
self._assign_max_widths(
stdout, x, int(parsed_args.max_width), min_width,
parsed_args.fit_width)
formatted = x.get_string()
stdout.write(formatted)
stdout.write('\n')
return
def emit_one(self, column_names, data, stdout, parsed_args):
x = prettytable.PrettyTable(field_names=('Field', 'Value'),
print_empty=False)
x.padding_width = 1
# Align all columns left because the values are
# not all the same type.
x.align['Field'] = 'l'
x.align['Value'] = 'l'
for name, value in zip(column_names, data):
x.add_row(_format_row((name, value)))
# Choose a reasonable min_width to better handle a narrow
# console. The table will overflow the console width in preference
# to wrapping columns smaller than 16 characters in an attempt to keep
# the Field column readable.
min_width = 16
self._assign_max_widths(
stdout, x, int(parsed_args.max_width), min_width,
parsed_args.fit_width)
formatted = x.get_string()
stdout.write(formatted)
stdout.write('\n')
return
@staticmethod
def _field_widths(field_names, first_line):
# use the first line +----+-------+ to infer column widths
# accounting for padding and dividers
widths = [max(0, len(i) - 2) for i in first_line.split('+')[1:-1]]
return dict(zip(field_names, widths))
@staticmethod
def _width_info(term_width, field_count):
# remove padding and dividers for width available to actual content
usable_total_width = max(0, term_width - 1 - 3 * field_count)
# calculate width per column if all columns were equal
if field_count == 0:
optimal_width = 0
else:
optimal_width = max(0, usable_total_width // field_count)
return usable_total_width, optimal_width
@staticmethod
def _build_shrink_fields(usable_total_width, optimal_width,
field_widths, field_names):
shrink_fields = []
shrink_remaining = usable_total_width
for field in field_names:
w = field_widths[field]
if w <= optimal_width:
# leave alone columns which are smaller than the optimal width
shrink_remaining -= w
else:
shrink_fields.append(field)
return shrink_fields, shrink_remaining
@staticmethod
def _assign_max_widths(stdout, x, max_width, min_width=0, fit_width=False):
if min_width:
x.min_width = min_width
if max_width > 0:
term_width = max_width
elif not fit_width:
# Fitting is disabled
return
else:
term_width = utils.terminal_width(stdout)
if not term_width:
# not a tty, so do not set any max widths
return
field_count = len(x.field_names)
try:
first_line = x.get_string().splitlines()[0]
if len(first_line) <= term_width:
return
except IndexError:
return
usable_total_width, optimal_width = TableFormatter._width_info(
term_width, field_count)
field_widths = TableFormatter._field_widths(x.field_names, first_line)
shrink_fields, shrink_remaining = TableFormatter._build_shrink_fields(
usable_total_width, optimal_width, field_widths, x.field_names)
shrink_to = shrink_remaining // len(shrink_fields)
# make all shrinkable fields size shrink_to apart from the last one
for field in shrink_fields[:-1]:
x.max_width[field] = max(min_width, shrink_to)
shrink_remaining -= shrink_to
# give the last shrinkable column shrink_to plus any remaining
field = shrink_fields[-1]
x.max_width[field] = max(min_width, shrink_remaining)

View File

@@ -1,44 +0,0 @@
# 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.
"""Output formatters values only
"""
import six
from . import base
from cliff import columns
class ValueFormatter(base.ListFormatter, base.SingleFormatter):
def add_argument_group(self, parser):
pass
def emit_list(self, column_names, data, stdout, parsed_args):
for row in data:
stdout.write(
' '.join(
six.text_type(c.machine_readable()
if isinstance(c, columns.FormattableColumn)
else c)
for c in row) + u'\n')
return
def emit_one(self, column_names, data, stdout, parsed_args):
for value in data:
stdout.write('%s\n' % six.text_type(
value.machine_readable()
if isinstance(value, columns.FormattableColumn)
else value)
)
return

View File

@@ -1,45 +0,0 @@
# 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.
"""Output formatters using PyYAML.
"""
import yaml
from . import base
from cliff import columns
class YAMLFormatter(base.ListFormatter, base.SingleFormatter):
def add_argument_group(self, parser):
pass
def emit_list(self, column_names, data, stdout, parsed_args):
items = []
for item in data:
items.append(
{n: (i.machine_readable()
if isinstance(i, columns.FormattableColumn)
else i)
for n, i in zip(column_names, item)}
)
yaml.safe_dump(items, stream=stdout, default_flow_style=False)
def emit_one(self, column_names, data, stdout, parsed_args):
for key, value in zip(column_names, data):
dict_data = {
key: (value.machine_readable()
if isinstance(value, columns.FormattableColumn)
else value)
}
yaml.safe_dump(dict_data, stream=stdout, default_flow_style=False)

View File

@@ -1,103 +0,0 @@
# 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 argparse
import inspect
import sys
import traceback
from . import command
class HelpAction(argparse.Action):
"""Provide a custom action so the -h and --help options
to the main app will print a list of the commands.
The commands are determined by checking the CommandManager
instance, passed in as the "default" value for the action.
"""
def __call__(self, parser, namespace, values, option_string=None):
app = self.default
parser.print_help(app.stdout)
app.stdout.write('\nCommands:\n')
command_manager = app.command_manager
for name, ep in sorted(command_manager):
try:
factory = ep.load()
except Exception:
app.stdout.write('Could not load %r\n' % ep)
if namespace.debug:
traceback.print_exc(file=app.stdout)
continue
try:
kwargs = {}
if 'cmd_name' in inspect.getargspec(factory.__init__).args:
kwargs['cmd_name'] = name
cmd = factory(app, None, **kwargs)
if cmd.deprecated:
continue
except Exception as err:
app.stdout.write('Could not instantiate %r: %s\n' % (ep, err))
if namespace.debug:
traceback.print_exc(file=app.stdout)
continue
one_liner = cmd.get_description().split('\n')[0]
app.stdout.write(' %-13s %s\n' % (name, one_liner))
sys.exit(0)
class HelpCommand(command.Command):
"""print detailed help for another command
"""
def get_parser(self, prog_name):
parser = super(HelpCommand, self).get_parser(prog_name)
parser.add_argument('cmd',
nargs='*',
help='name of the command',
)
return parser
def take_action(self, parsed_args):
if parsed_args.cmd:
try:
the_cmd = self.app.command_manager.find_command(
parsed_args.cmd,
)
cmd_factory, cmd_name, search_args = the_cmd
except ValueError:
# Did not find an exact match
cmd = parsed_args.cmd[0]
fuzzy_matches = [k[0] for k in self.app.command_manager
if k[0].startswith(cmd)
]
if not fuzzy_matches:
raise
self.app.stdout.write('Command "%s" matches:\n' % cmd)
for fm in sorted(fuzzy_matches):
self.app.stdout.write(' %s\n' % fm)
return
self.app_args.cmd = search_args
kwargs = {}
if 'cmd_name' in inspect.getargspec(cmd_factory.__init__).args:
kwargs['cmd_name'] = cmd_name
cmd = cmd_factory(self.app, self.app_args, **kwargs)
full_name = (cmd_name
if self.app.interactive_mode
else ' '.join([self.app.NAME, cmd_name])
)
cmd_parser = cmd.get_parser(full_name)
cmd_parser.print_help(self.app.stdout)
else:
action = HelpAction(None, None, default=self.app)
action(self.app.parser, self.app.options, None, None)
return 0

View File

@@ -1,65 +0,0 @@
# 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 abc
import six
@six.add_metaclass(abc.ABCMeta)
class CommandHook(object):
"""Base class for command hooks.
:param app: Command instance being invoked
:paramtype app: cliff.command.Command
"""
def __init__(self, command):
self.cmd = command
@abc.abstractmethod
def get_parser(self, parser):
"""Return an :class:`argparse.ArgumentParser`.
:param parser: An existing ArgumentParser instance to be modified.
:paramtype parser: ArgumentParser
:returns: ArgumentParser
"""
return parser
@abc.abstractmethod
def get_epilog(self):
"Return text to add to the command help epilog."
return ''
@abc.abstractmethod
def before(self, parsed_args):
"""Called before the command's take_action() method.
Any return value is ignored.
:param parsed_args: The arguments to the command.
:paramtype parsed_args: argparse.Namespace
"""
@abc.abstractmethod
def after(self, parsed_args, return_code):
"""Called after the command's take_action() method.
Any return value is ignored.
:param parsed_args: The arguments to the command.
:paramtype parsed_args: argparse.Namespace
:param return_code: The value returned from take_action().
:paramtype return_code: int
"""

View File

@@ -1,147 +0,0 @@
# 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.
"""Application base class.
"""
import itertools
import shlex
import sys
import cmd2
class InteractiveApp(cmd2.Cmd):
"""Provides "interactive mode" features.
Refer to the cmd2_ and cmd_ documentation for details
about subclassing and configuring this class.
.. _cmd2: http://packages.python.org/cmd2/index.html
.. _cmd: http://docs.python.org/library/cmd.html
:param parent_app: The calling application (expected to be derived
from :class:`cliff.main.App`).
:param command_manager: A :class:`cliff.commandmanager.CommandManager`
instance.
:param stdin: Standard input stream
:param stdout: Standard output stream
"""
use_rawinput = True
doc_header = "Shell commands (type help <topic>):"
app_cmd_header = "Application commands (type help <topic>):"
def __init__(self, parent_app, command_manager, stdin, stdout):
self.parent_app = parent_app
if not hasattr(sys.stdin, 'isatty') or sys.stdin.isatty():
self.prompt = '(%s) ' % parent_app.NAME
else:
# batch/pipe mode
self.prompt = ''
self.command_manager = command_manager
cmd2.Cmd.__init__(self, 'tab', stdin=stdin, stdout=stdout)
def default(self, line):
# Tie in the default command processor to
# dispatch commands known to the command manager.
# We send the message through our parent app,
# since it already has the logic for executing
# the subcommand.
line_parts = shlex.split(line.parsed.raw)
self.parent_app.run_subcommand(line_parts)
def completenames(self, text, line, begidx, endidx):
"""Tab-completion for command prefix without completer delimiter.
This method returns cmd style and cliff style commands matching
provided command prefix (text).
"""
completions = cmd2.Cmd.completenames(self, text, line, begidx, endidx)
completions += self._complete_prefix(text)
return completions
def completedefault(self, text, line, begidx, endidx):
"""Default tab-completion for command prefix with completer delimiter.
This method filters only cliff style commands matching provided
command prefix (line) as cmd2 style commands cannot contain spaces.
This method returns text + missing command part of matching commands.
This method does not handle options in cmd2/cliff style commands, you
must define complete_$method to handle them.
"""
return [x[begidx:] for x in self._complete_prefix(line)]
def _complete_prefix(self, prefix):
"""Returns cliff style commands with a specific prefix."""
if not prefix:
return [n for n, v in self.command_manager]
return [n for n, v in self.command_manager if n.startswith(prefix)]
def help_help(self):
# Use the command manager to get instructions for "help"
self.default('help help')
def do_help(self, arg):
if arg:
# Check if the arg is a builtin command or something
# coming from the command manager
arg_parts = shlex.split(arg)
method_name = '_'.join(
itertools.chain(
['do'],
itertools.takewhile(lambda x: not x.startswith('-'),
arg_parts)
)
)
# Have the command manager version of the help
# command produce the help text since cmd and
# cmd2 do not provide help for "help"
if hasattr(self, method_name):
return cmd2.Cmd.do_help(self, arg)
# Dispatch to the underlying help command,
# which knows how to provide help for extension
# commands.
self.default(self.parsed('help ' + arg))
else:
cmd2.Cmd.do_help(self, arg)
cmd_names = sorted([n for n, v in self.command_manager])
self.print_topics(self.app_cmd_header, cmd_names, 15, 80)
return
def get_names(self):
# Override the base class version to filter out
# things that look like they should be hidden
# from the user.
return [n
for n in cmd2.Cmd.get_names(self)
if not n.startswith('do__')
]
def precmd(self, statement):
# Pre-process the parsed command in case it looks like one of
# our subcommands, since cmd2 does not handle multi-part
# command names by default.
line_parts = shlex.split(statement.parsed.raw)
try:
the_cmd = self.command_manager.find_command(line_parts)
cmd_factory, cmd_name, sub_argv = the_cmd
except ValueError:
# Not a plugin command
pass
else:
statement.parsed.command = cmd_name
statement.parsed.args = ' '.join(sub_argv)
return statement
def cmdloop(self):
self._cmdloop()

View File

@@ -1,55 +0,0 @@
# 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.
"""Application base class for providing a list of data as output.
"""
import abc
import six
from . import display
@six.add_metaclass(abc.ABCMeta)
class Lister(display.DisplayCommandBase):
"""Command base class for providing a list of data as output.
"""
@property
def formatter_namespace(self):
return 'cliff.formatter.list'
@property
def formatter_default(self):
return 'table'
@abc.abstractmethod
def take_action(self, parsed_args):
"""Return a tuple containing the column names and an iterable
containing the data to be listed.
"""
def produce_output(self, parsed_args, column_names, data):
(columns_to_include, selector) = self._generate_columns_and_selector(
parsed_args, column_names)
if selector:
# Generator expression to only return the parts of a row
# of data that the user has expressed interest in
# seeing. We have to convert the compress() output to a
# list so the table formatter can ask for its length.
data = (list(self._compress_iterable(row, selector))
for row in data)
self.formatter.emit_list(columns_to_include,
data,
self.app.stdout,
parsed_args,
)
return 0

View File

@@ -1,59 +0,0 @@
# 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.
"""Application base class for displaying data about a single object.
"""
import abc
import six
from . import display
@six.add_metaclass(abc.ABCMeta)
class ShowOne(display.DisplayCommandBase):
"""Command base class for displaying data about a single object.
"""
@property
def formatter_namespace(self):
return 'cliff.formatter.show'
@property
def formatter_default(self):
return 'table'
@abc.abstractmethod
def take_action(self, parsed_args):
"""Return a two-part tuple with a tuple of column names
and a tuple of values.
"""
def produce_output(self, parsed_args, column_names, data):
(columns_to_include, selector) = self._generate_columns_and_selector(
parsed_args, column_names)
if selector:
data = list(self._compress_iterable(data, selector))
self.formatter.emit_one(columns_to_include,
data,
self.app.stdout,
parsed_args)
return 0
def dict2columns(self, data):
"""Implement the common task of converting a dict-based object
to the two-column output that ShowOne expects.
"""
if not data:
return ({}, {})
else:
return zip(*sorted(data.items()))

View File

@@ -1,307 +0,0 @@
# Copyright (C) 2017, Red Hat, Inc.
#
# 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 argparse
import fnmatch
import re
from docutils import nodes
from docutils.parsers import rst
from docutils.parsers.rst import directives
from docutils import statemachine
from cliff import commandmanager
def _indent(text):
"""Indent by four spaces."""
prefix = ' ' * 4
def prefixed_lines():
for line in text.splitlines(True):
yield (prefix + line if line.strip() else line)
return ''.join(prefixed_lines())
def _format_description(parser):
"""Get parser description.
We parse this as reStructuredText, allowing users to embed rich
information in their help messages if they so choose.
"""
for line in statemachine.string2lines(
parser.description, tab_width=4, convert_whitespace=True):
yield line
def _format_usage(parser):
"""Get usage without a prefix."""
fmt = argparse.HelpFormatter(parser.prog)
optionals = parser._get_optional_actions()
positionals = parser._get_positional_actions()
groups = parser._mutually_exclusive_groups
# hacked variant of the regex used by the actual argparse module. Unlike
# that version, this one attempts to group long and short opts with their
# optional arguments ensuring that, for example, '---format <FORMAT>'
# becomes ['--format <FORMAT>'] and not ['--format', '<FORMAT>'].
# Yes, they really do use regexes to break apart and rewrap their help
# string. Don't ask me why.
part_regexp = r'\(.*?\)+|\[.*?\]+|(?:(?:-\w|--\w+)(?:\s+<\w+>)?)|\S+'
opt_usage = fmt._format_actions_usage(optionals, groups)
pos_usage = fmt._format_actions_usage(positionals, groups)
opt_parts = re.findall(part_regexp, opt_usage)
pos_parts = re.findall(part_regexp, pos_usage)
parts = opt_parts + pos_parts
if len(' '.join([parser.prog] + parts)) < 72:
return [' '.join([parser.prog] + parts)]
return [parser.prog] + [_indent(x) for x in parts]
def _format_epilog(parser):
"""Get parser epilog.
We parse this as reStructuredText, allowing users to embed rich
information in their help messages if they so choose.
"""
for line in statemachine.string2lines(
parser.epilog, tab_width=4, convert_whitespace=True):
yield line
def _format_positional_action(action):
"""Format a positional action."""
if action.help == argparse.SUPPRESS:
return
# NOTE(stephenfin): We strip all types of brackets from 'metavar' because
# the 'option' directive dictates that only option argument names should be
# surrounded by angle brackets
yield '.. option:: {}'.format(
(action.metavar or action.dest).strip('<>[]() '))
if action.help:
yield ''
for line in statemachine.string2lines(
action.help, tab_width=4, convert_whitespace=True):
yield _indent(line)
def _format_optional_action(action):
"""Format an optional action."""
if action.help == argparse.SUPPRESS:
return
if action.nargs == 0:
yield '.. option:: {}'.format(', '.join(action.option_strings))
else:
# TODO(stephenfin): At some point, we may wish to provide more
# information about the options themselves, for example, if nargs is
# specified
option_strings = [' '.join(
[x, action.metavar or '<{}>'.format(action.dest.upper())])
for x in action.option_strings]
yield '.. option:: {}'.format(', '.join(option_strings))
if action.help:
yield ''
for line in statemachine.string2lines(
action.help, tab_width=4, convert_whitespace=True):
yield _indent(line)
def _format_parser(parser):
"""Format the output of an argparse 'ArgumentParser' object.
Given the following parser::
>>> import argparse
>>> parser = argparse.ArgumentParser(prog='hello-world', \
description='This is my description.',
epilog='This is my epilog')
>>> parser.add_argument('name', help='User name', metavar='<name>')
>>> parser.add_argument('--language', action='store', dest='lang', \
help='Greeting language')
Returns the following::
This is my description.
.. program:: hello-world
.. code:: shell
hello-world [-h] [--language LANG] <name>
.. option:: name
User name
.. option:: --language LANG
Greeting language
.. option:: -h, --help
Show this help message and exit
This is my epilog.
"""
if parser.description:
for line in _format_description(parser):
yield line
yield ''
yield '.. program:: {}'.format(parser.prog)
yield '.. code-block:: shell'
yield ''
for line in _format_usage(parser):
yield _indent(line)
yield ''
# In argparse, all arguments and parameters are known as "actions".
# Optional actions are what would be known as flags or options in other
# libraries, while positional actions would generally be known as
# arguments. We present these slightly differently.
for action in parser._get_optional_actions():
for line in _format_optional_action(action):
yield line
yield ''
for action in parser._get_positional_actions():
for line in _format_positional_action(action):
yield line
yield ''
if parser.epilog:
for line in _format_epilog(parser):
yield line
yield ''
class AutoprogramCliffDirective(rst.Directive):
"""Auto-document a subclass of `cliff.command.Command`."""
has_content = False
required_arguments = 1
option_spec = {
'command': directives.unchanged,
'ignored': directives.unchanged,
'application': directives.unchanged,
}
def _load_command(self, manager, command_name):
"""Load a command using an instance of a `CommandManager`."""
try:
# find_command expects the value of argv so split to emulate that
return manager.find_command(command_name.split())[0]
except ValueError:
raise self.error('"{}" is not a valid command in the "{}" '
'namespace'.format(
command_name, manager.namespace))
def _generate_nodes(self, title, command_name, command_class,
ignored_opts):
"""Generate the relevant Sphinx nodes.
This is a little funky. Parts of this use raw docutils nodes while
other parts use reStructuredText and nested parsing. The reason for
this is simple: it avoids us having to reinvent the wheel. While raw
docutils nodes are helpful for the simpler elements of the output,
they don't provide an easy way to use Sphinx's own directives, such as
the 'option' directive. Refer to [1] for more information.
[1] http://www.sphinx-doc.org/en/stable/extdev/markupapi.html
:param title: Title of command
:param command_name: Name of command, as used on the command line
:param command_class: Subclass of :py:class:`cliff.command.Command`
:param prefix: Prefix to apply before command, if any
:param ignored_opts: A list of options to exclude from output, if any
:returns: A list of nested docutil nodes
"""
command = command_class(None, None)
parser = command.get_parser(command_name)
ignored_opts = ignored_opts or []
# Drop the automatically-added help action
for action in list(parser._actions):
for option_string in action.option_strings:
if option_string in ignored_opts:
del parser._actions[parser._actions.index(action)]
break
section = nodes.section(
'',
nodes.title(text=title),
ids=[nodes.make_id(title)],
names=[nodes.fully_normalize_name(title)])
source_name = '<{}>'.format(command.__class__.__name__)
result = statemachine.ViewList()
for line in _format_parser(parser):
result.append(line, source_name)
self.state.nested_parse(result, 0, section)
return [section]
def run(self):
self.env = self.state.document.settings.env
command_pattern = self.options.get('command')
application_name = (self.options.get('application')
or self.env.config.autoprogram_cliff_application)
global_ignored = self.env.config.autoprogram_cliff_ignored
local_ignored = self.options.get('ignored', '')
local_ignored = [x.strip() for x in local_ignored.split(',')
if x.strip()]
ignored_opts = list(set(global_ignored + local_ignored))
# TODO(sfinucan): We should probably add this wildcarding functionality
# to the CommandManager itself to allow things like "show me the
# commands like 'foo *'"
manager = commandmanager.CommandManager(self.arguments[0])
if command_pattern:
commands = [x for x in manager.commands
if fnmatch.fnmatch(x, command_pattern)]
else:
commands = manager.commands.keys()
output = []
for command_name in sorted(commands):
command_class = self._load_command(manager, command_name)
title = command_name
if application_name:
command_name = ' '.join([application_name, command_name])
output.extend(self._generate_nodes(
title, command_name, command_class, ignored_opts))
return output
def setup(app):
app.add_directive('autoprogram-cliff', AutoprogramCliffDirective)
app.add_config_value('autoprogram_cliff_application', '', True)
app.add_config_value('autoprogram_cliff_ignored', ['--help'], True)

View File

@@ -1,29 +0,0 @@
# -*- encoding: utf-8 -*-
#
# 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 testtools
import fixtures
class TestBase(testtools.TestCase):
def setUp(self):
super(TestBase, self).setUp()
self._stdout_fixture = fixtures.StringStream('stdout')
self.stdout = self.useFixture(self._stdout_fixture).stream
self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.stdout))
self._stderr_fixture = fixtures.StringStream('stderr')
self.stderr = self.useFixture(self._stderr_fixture).stream
self.useFixture(fixtures.MonkeyPatch('sys.stderr', self.stderr))

View File

@@ -1,500 +0,0 @@
# -*- encoding: utf-8 -*-
#
# 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 argparse
import codecs
import locale
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
import sys
import mock
import six
from cliff import app as application
from cliff import command as c_cmd
from cliff import commandmanager
from cliff.tests import base
from cliff.tests import utils as test_utils
from cliff import utils
def make_app(**kwargs):
cmd_mgr = commandmanager.CommandManager('cliff.tests')
# Register a command that succeeds
command = mock.MagicMock(spec=c_cmd.Command)
command_inst = mock.MagicMock(spec=c_cmd.Command)
command_inst.run.return_value = 0
command.return_value = command_inst
cmd_mgr.add_command('mock', command)
# Register a command that fails
err_command = mock.Mock(name='err_command', spec=c_cmd.Command)
err_command_inst = mock.Mock(spec=c_cmd.Command)
err_command_inst.run = mock.Mock(
side_effect=RuntimeError('test exception')
)
err_command.return_value = err_command_inst
cmd_mgr.add_command('error', err_command)
app = application.App('testing interactive mode',
'1',
cmd_mgr,
stderr=mock.Mock(), # suppress warning messages
**kwargs
)
return app, command
class TestInteractiveMode(base.TestBase):
def test_no_args_triggers_interactive_mode(self):
app, command = make_app()
app.interact = mock.MagicMock(name='inspect')
app.run([])
app.interact.assert_called_once_with()
def test_interactive_mode_cmdloop(self):
app, command = make_app()
app.interactive_app_factory = mock.MagicMock(
name='interactive_app_factory'
)
self.assertIs(None, app.interpreter)
app.run([])
self.assertIsNot(None, app.interpreter)
cmdloop = app.interactive_app_factory.return_value.cmdloop
cmdloop.assert_called_once_with()
class TestInitAndCleanup(base.TestBase):
def test_initialize_app(self):
app, command = make_app()
app.initialize_app = mock.MagicMock(name='initialize_app')
app.run(['mock'])
app.initialize_app.assert_called_once_with(['mock'])
def test_prepare_to_run_command(self):
app, command = make_app()
app.prepare_to_run_command = mock.MagicMock(
name='prepare_to_run_command',
)
app.run(['mock'])
app.prepare_to_run_command.assert_called_once_with(command())
def test_clean_up_success(self):
app, command = make_app()
app.clean_up = mock.MagicMock(name='clean_up')
app.run(['mock'])
app.clean_up.assert_called_once_with(command.return_value, 0, None)
def test_clean_up_error(self):
app, command = make_app()
app.clean_up = mock.MagicMock(name='clean_up')
app.run(['error'])
app.clean_up.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY)
call_args = app.clean_up.call_args_list[0]
self.assertEqual(mock.call(mock.ANY, 1, mock.ANY), call_args)
args, kwargs = call_args
self.assertIsInstance(args[2], RuntimeError)
self.assertEqual(('test exception',), args[2].args)
def test_clean_up_error_debug(self):
app, command = make_app()
app.clean_up = mock.MagicMock(name='clean_up')
try:
app.run(['--debug', 'error'])
except RuntimeError as err:
self.assertIs(err, app.clean_up.call_args_list[0][0][2])
else:
self.fail('Should have had an exception')
self.assertTrue(app.clean_up.called)
call_args = app.clean_up.call_args_list[0]
self.assertEqual(mock.call(mock.ANY, 1, mock.ANY), call_args)
args, kwargs = call_args
self.assertIsInstance(args[2], RuntimeError)
self.assertEqual(('test exception',), args[2].args)
def test_error_handling_clean_up_raises_exception(self):
app, command = make_app()
app.clean_up = mock.MagicMock(
name='clean_up',
side_effect=RuntimeError('within clean_up'),
)
app.run(['error'])
self.assertTrue(app.clean_up.called)
call_args = app.clean_up.call_args_list[0]
self.assertEqual(mock.call(mock.ANY, 1, mock.ANY), call_args)
args, kwargs = call_args
self.assertIsInstance(args[2], RuntimeError)
self.assertEqual(('test exception',), args[2].args)
def test_error_handling_clean_up_raises_exception_debug(self):
app, command = make_app()
app.clean_up = mock.MagicMock(
name='clean_up',
side_effect=RuntimeError('within clean_up'),
)
try:
app.run(['--debug', 'error'])
except RuntimeError as err:
if not hasattr(err, '__context__'):
# The exception passed to clean_up is not the exception
# caused *by* clean_up. This test is only valid in python
# 2 because under v3 the original exception is re-raised
# with the new one as a __context__ attribute.
self.assertIsNot(err, app.clean_up.call_args_list[0][0][2])
else:
self.fail('Should have had an exception')
self.assertTrue(app.clean_up.called)
call_args = app.clean_up.call_args_list[0]
self.assertEqual(mock.call(mock.ANY, 1, mock.ANY), call_args)
args, kwargs = call_args
self.assertIsInstance(args[2], RuntimeError)
self.assertEqual(('test exception',), args[2].args)
def test_normal_clean_up_raises_exception(self):
app, command = make_app()
app.clean_up = mock.MagicMock(
name='clean_up',
side_effect=RuntimeError('within clean_up'),
)
app.run(['mock'])
self.assertTrue(app.clean_up.called)
call_args = app.clean_up.call_args_list[0]
self.assertEqual(mock.call(mock.ANY, 0, None), call_args)
def test_normal_clean_up_raises_exception_debug(self):
app, command = make_app()
app.clean_up = mock.MagicMock(
name='clean_up',
side_effect=RuntimeError('within clean_up'),
)
app.run(['--debug', 'mock'])
self.assertTrue(app.clean_up.called)
call_args = app.clean_up.call_args_list[0]
self.assertEqual(mock.call(mock.ANY, 0, None), call_args)
class TestOptionParser(base.TestBase):
def test_conflicting_option_should_throw(self):
class MyApp(application.App):
def __init__(self):
super(MyApp, self).__init__(
description='testing',
version='0.1',
command_manager=commandmanager.CommandManager('tests'),
)
def build_option_parser(self, description, version):
parser = super(MyApp, self).build_option_parser(description,
version)
parser.add_argument(
'-h', '--help',
default=self, # tricky
help="Show help message and exit.",
)
self.assertRaises(
argparse.ArgumentError,
MyApp,
)
def test_conflicting_option_custom_arguments_should_not_throw(self):
class MyApp(application.App):
def __init__(self):
super(MyApp, self).__init__(
description='testing',
version='0.1',
command_manager=commandmanager.CommandManager('tests'),
)
def build_option_parser(self, description, version):
argparse_kwargs = {'conflict_handler': 'resolve'}
parser = super(MyApp, self).build_option_parser(
description,
version,
argparse_kwargs=argparse_kwargs)
parser.add_argument(
'-h', '--help',
default=self, # tricky
help="Show help message and exit.",
)
MyApp()
def test_option_parser_abbrev_issue(self):
class MyCommand(c_cmd.Command):
def get_parser(self, prog_name):
parser = super(MyCommand, self).get_parser(prog_name)
parser.add_argument("--end")
return parser
def take_action(self, parsed_args):
assert(parsed_args.end == '123')
class MyCommandManager(commandmanager.CommandManager):
def load_commands(self, namespace):
self.add_command("mycommand", MyCommand)
class MyApp(application.App):
def __init__(self):
super(MyApp, self).__init__(
description='testing',
version='0.1',
command_manager=MyCommandManager(None),
)
def build_option_parser(self, description, version):
parser = super(MyApp, self).build_option_parser(
description,
version,
argparse_kwargs={'allow_abbrev': False})
parser.add_argument('--endpoint')
return parser
app = MyApp()
# NOTE(jd) --debug is necessary so assert in take_action()
# raises correctly here
app.run(['--debug', 'mycommand', '--end', '123'])
class TestHelpHandling(base.TestBase):
def _test_help(self, deferred_help):
app, _ = make_app(deferred_help=deferred_help)
with mock.patch.object(app, 'initialize_app') as init:
with mock.patch('cliff.help.HelpAction.__call__',
side_effect=SystemExit(0)) as helper:
self.assertRaises(
SystemExit,
app.run,
['--help'],
)
self.assertTrue(helper.called)
self.assertEqual(deferred_help, init.called)
def test_help(self):
self._test_help(False)
def test_deferred_help(self):
self._test_help(True)
def test_subcommand_help(self):
app, _ = make_app(deferred_help=False)
# Help is called immediately
with mock.patch('cliff.help.HelpAction.__call__') as helper:
app.run(['show', 'files', '--help'])
self.assertTrue(helper.called)
def test_subcommand_deferred_help(self):
app, _ = make_app(deferred_help=True)
# Show that provide_help_if_requested() did not show help and exit
with mock.patch.object(app, 'run_subcommand') as helper:
app.run(['show', 'files', '--help'])
helper.assert_called_once_with(['help', 'show', 'files'])
class TestCommandLookup(base.TestBase):
def test_unknown_cmd(self):
app, command = make_app()
self.assertEqual(2, app.run(['hell']))
def test_unknown_cmd_debug(self):
app, command = make_app()
try:
self.assertEqual(2, app.run(['--debug', 'hell']))
except ValueError as err:
self.assertIn("['hell']", str(err))
def test_list_matching_commands(self):
stdout = StringIO()
app = application.App('testing', '1',
test_utils.TestCommandManager(
test_utils.TEST_NAMESPACE),
stdout=stdout)
app.NAME = 'test'
try:
self.assertEqual(2, app.run(['t']))
except SystemExit:
pass
output = stdout.getvalue()
self.assertIn("test: 't' is not a test command. See 'test --help'.",
output)
self.assertIn('Did you mean one of these?', output)
self.assertIn('three word command\n two words\n', output)
def test_fuzzy_no_commands(self):
cmd_mgr = commandmanager.CommandManager('cliff.fuzzy')
app = application.App('test', '1.0', cmd_mgr)
cmd_mgr.commands = {}
matches = app.get_fuzzy_matches('foo')
self.assertEqual([], matches)
def test_fuzzy_common_prefix(self):
# searched string is a prefix of all commands
cmd_mgr = commandmanager.CommandManager('cliff.fuzzy')
app = application.App('test', '1.0', cmd_mgr)
cmd_mgr.commands = {}
cmd_mgr.add_command('user list', test_utils.TestCommand)
cmd_mgr.add_command('user show', test_utils.TestCommand)
matches = app.get_fuzzy_matches('user')
self.assertEqual(['user list', 'user show'], matches)
def test_fuzzy_same_distance(self):
# searched string has the same distance to all commands
cmd_mgr = commandmanager.CommandManager('cliff.fuzzy')
app = application.App('test', '1.0', cmd_mgr)
cmd_mgr.add_command('user', test_utils.TestCommand)
for cmd in cmd_mgr.commands.keys():
self.assertEqual(
8,
utils.damerau_levenshtein('node', cmd, utils.COST),
)
matches = app.get_fuzzy_matches('node')
self.assertEqual(['complete', 'help', 'user'], matches)
def test_fuzzy_no_prefix(self):
# search by distance, no common prefix with any command
cmd_mgr = commandmanager.CommandManager('cliff.fuzzy')
app = application.App('test', '1.0', cmd_mgr)
cmd_mgr.add_command('user', test_utils.TestCommand)
matches = app.get_fuzzy_matches('uesr')
self.assertEqual(['user'], matches)
class TestVerboseMode(base.TestBase):
def test_verbose(self):
app, command = make_app()
app.clean_up = mock.MagicMock(name='clean_up')
app.run(['--verbose', 'mock'])
app.clean_up.assert_called_once_with(command.return_value, 0, None)
app.clean_up.reset_mock()
app.run(['--quiet', 'mock'])
app.clean_up.assert_called_once_with(command.return_value, 0, None)
self.assertRaises(
SystemExit,
app.run,
['--verbose', '--quiet', 'mock'],
)
class TestIO(base.TestBase):
def test_io_streams(self):
cmd_mgr = commandmanager.CommandManager('cliff.tests')
io = mock.Mock()
if six.PY2:
stdin_save = sys.stdin
stdout_save = sys.stdout
stderr_save = sys.stderr
encoding = locale.getpreferredencoding() or 'utf-8'
app = application.App('no io streams', 1, cmd_mgr)
self.assertIsInstance(app.stdin, codecs.StreamReader)
self.assertIsInstance(app.stdout, codecs.StreamWriter)
self.assertIsInstance(app.stderr, codecs.StreamWriter)
app = application.App('with stdin io stream', 1, cmd_mgr, stdin=io)
self.assertIs(io, app.stdin)
self.assertIsInstance(app.stdout, codecs.StreamWriter)
self.assertIsInstance(app.stderr, codecs.StreamWriter)
app = application.App('with stdout io stream', 1, cmd_mgr,
stdout=io)
self.assertIsInstance(app.stdin, codecs.StreamReader)
self.assertIs(io, app.stdout)
self.assertIsInstance(app.stderr, codecs.StreamWriter)
app = application.App('with stderr io stream', 1, cmd_mgr,
stderr=io)
self.assertIsInstance(app.stdin, codecs.StreamReader)
self.assertIsInstance(app.stdout, codecs.StreamWriter)
self.assertIs(io, app.stderr)
try:
sys.stdin = codecs.getreader(encoding)(sys.stdin)
app = application.App(
'with wrapped sys.stdin io stream', 1, cmd_mgr)
self.assertIs(sys.stdin, app.stdin)
self.assertIsInstance(app.stdout, codecs.StreamWriter)
self.assertIsInstance(app.stderr, codecs.StreamWriter)
finally:
sys.stdin = stdin_save
try:
sys.stdout = codecs.getwriter(encoding)(sys.stdout)
app = application.App('with wrapped stdout io stream', 1,
cmd_mgr)
self.assertIsInstance(app.stdin, codecs.StreamReader)
self.assertIs(sys.stdout, app.stdout)
self.assertIsInstance(app.stderr, codecs.StreamWriter)
finally:
sys.stdout = stdout_save
try:
sys.stderr = codecs.getwriter(encoding)(sys.stderr)
app = application.App('with wrapped stderr io stream', 1,
cmd_mgr)
self.assertIsInstance(app.stdin, codecs.StreamReader)
self.assertIsInstance(app.stdout, codecs.StreamWriter)
self.assertIs(sys.stderr, app.stderr)
finally:
sys.stderr = stderr_save
else:
app = application.App('no io streams', 1, cmd_mgr)
self.assertIs(sys.stdin, app.stdin)
self.assertIs(sys.stdout, app.stdout)
self.assertIs(sys.stderr, app.stderr)
app = application.App('with stdin io stream', 1, cmd_mgr, stdin=io)
self.assertIs(io, app.stdin)
self.assertIs(sys.stdout, app.stdout)
self.assertIs(sys.stderr, app.stderr)
app = application.App('with stdout io stream', 1, cmd_mgr,
stdout=io)
self.assertIs(sys.stdin, app.stdin)
self.assertIs(io, app.stdout)
self.assertIs(sys.stderr, app.stderr)
app = application.App('with stderr io stream', 1, cmd_mgr,
stderr=io)
self.assertIs(sys.stdin, app.stdin)
self.assertIs(sys.stdout, app.stdout)
self.assertIs(io, app.stderr)

View File

@@ -1,35 +0,0 @@
# 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 unittest
from cliff import columns
class FauxColumn(columns.FormattableColumn):
def human_readable(self):
return u'I made this string myself: {}'.format(self._value)
class TestColumns(unittest.TestCase):
def test_faux_column_machine(self):
c = FauxColumn(['list', 'of', 'values'])
self.assertEqual(['list', 'of', 'values'], c.machine_readable())
def test_faux_column_human(self):
c = FauxColumn(['list', 'of', 'values'])
self.assertEqual(
u"I made this string myself: ['list', 'of', 'values']",
c.human_readable(),
)

View File

@@ -1,130 +0,0 @@
# 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 functools
from cliff import command
from cliff.tests import base
class TestCommand(command.Command):
"""Description of command.
"""
def get_parser(self, prog_name):
parser = super(TestCommand, self).get_parser(prog_name)
parser.add_argument(
'long_help_argument',
help="Create a NIC on the server.\n"
"Specify option multiple times to create multiple NICs. "
"Either net-id or port-id must be provided, but not both.\n"
"net-id: attach NIC to network with this UUID\n"
"port-id: attach NIC to port with this UUID\n"
"v4-fixed-ip: IPv4 fixed address for NIC (optional)\n"
"v6-fixed-ip: IPv6 fixed address for NIC (optional)\n"
"none: (v2.37+) no network is attached\n"
"auto: (v2.37+) the compute service will automatically "
"allocate a network.\n"
"Specifying a --nic of auto or none "
"cannot be used with any other --nic value.",
)
parser.add_argument(
'regular_help_argument',
help="The quick brown fox jumps "
"over the lazy dog.",
)
return parser
def take_action(self, parsed_args):
return 42
class TestCommandNoDocstring(command.Command):
def take_action(self, parsed_args):
return 42
class TestDescription(base.TestBase):
def test_get_description_docstring(self):
cmd = TestCommand(None, None)
desc = cmd.get_description()
assert desc == "Description of command.\n "
def test_get_description_attribute(self):
cmd = TestCommand(None, None)
# Artificially inject a value for _description to verify that it
# overrides the docstring.
cmd._description = 'this is not the default'
desc = cmd.get_description()
assert desc == 'this is not the default'
def test_get_description_default(self):
cmd = TestCommandNoDocstring(None, None)
desc = cmd.get_description()
assert desc == ''
class TestBasicValues(base.TestBase):
def test_get_parser(self):
cmd = TestCommand(None, None)
parser = cmd.get_parser('NAME')
assert parser.prog == 'NAME'
def test_get_name(self):
cmd = TestCommand(None, None, cmd_name='object action')
assert cmd.cmd_name == 'object action'
def test_run_return(self):
cmd = TestCommand(None, None, cmd_name='object action')
assert cmd.run(None) == 42
expected_help_message = """
long_help_argument Create a NIC on the server.
Specify option multiple times to create multiple NICs.
Either net-id or port-id must be provided, but not
both.
net-id: attach NIC to network with this UUID
port-id: attach NIC to port with this UUID
v4-fixed-ip: IPv4 fixed address for NIC (optional)
v6-fixed-ip: IPv6 fixed address for NIC (optional)
none: (v2.37+) no network is attached
auto: (v2.37+) the compute service will automatically
allocate a network.
Specifying a --nic of auto or none cannot be used with
any other --nic value.
regular_help_argument
The quick brown fox jumps over the lazy dog.
"""
class TestHelp(base.TestBase):
def test_smart_help_formatter(self):
cmd = TestCommand(None, None)
parser = cmd.get_parser('NAME')
# Set up the formatter to always use a width=80 so that the
# terminal width of the developer's system does not cause the
# test to fail. Trying to mock os.environ failed, but there is
# an arg to HelpFormatter to set the width
# explicitly. Unfortunately, there is no way to do that
# through the parser, so we have to replace the parser's
# formatter_class attribute with a partial() that passes width
# to the original class.
parser.formatter_class = functools.partial(
parser.formatter_class,
width=78,
)
self.assertIn(expected_help_message, parser.format_help())

View File

@@ -1,235 +0,0 @@
# 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 cliff import app as application
from cliff import command
from cliff import commandmanager
from cliff import hooks
from cliff import lister
from cliff import show
from cliff.tests import base
import mock
from stevedore import extension
def make_app(**kwargs):
cmd_mgr = commandmanager.CommandManager('cliff.tests')
# Register a command that succeeds
cmd = mock.MagicMock(spec=command.Command)
command_inst = mock.MagicMock(spec=command.Command)
command_inst.run.return_value = 0
cmd.return_value = command_inst
cmd_mgr.add_command('mock', cmd)
# Register a command that fails
err_command = mock.Mock(name='err_command', spec=command.Command)
err_command_inst = mock.Mock(spec=command.Command)
err_command_inst.run = mock.Mock(
side_effect=RuntimeError('test exception')
)
err_command.return_value = err_command_inst
cmd_mgr.add_command('error', err_command)
app = application.App('testing command hooks',
'1',
cmd_mgr,
stderr=mock.Mock(), # suppress warning messages
**kwargs
)
return app
class TestCommand(command.Command):
"""Description of command.
"""
def get_parser(self, prog_name):
parser = super(TestCommand, self).get_parser(prog_name)
return parser
def take_action(self, parsed_args):
return 42
class TestShowCommand(show.ShowOne):
"""Description of command.
"""
def take_action(self, parsed_args):
return (('Name',), ('value',))
class TestListerCommand(lister.Lister):
"""Description of command.
"""
def take_action(self, parsed_args):
return (('Name',), [('value',)])
class TestHook(hooks.CommandHook):
_before_called = False
_after_called = False
def get_parser(self, parser):
parser.add_argument('--added-by-hook')
return parser
def get_epilog(self):
return 'hook epilog'
def before(self, parsed_args):
self._before_called = True
def after(self, parsed_args, return_code):
self._after_called = True
class TestCommandLoadHooks(base.TestBase):
def test_no_app_or_name(self):
cmd = TestCommand(None, None)
self.assertEqual([], cmd._hooks)
@mock.patch('stevedore.extension.ExtensionManager')
def test_app_and_name(self, em):
app = make_app()
TestCommand(app, None, cmd_name='test')
print(em.mock_calls[0])
name, args, kwargs = em.mock_calls[0]
print(kwargs)
self.assertEqual('cliff.tests.test', kwargs['namespace'])
class TestHooks(base.TestBase):
def setUp(self):
super(TestHooks, self).setUp()
self.app = make_app()
self.cmd = TestCommand(self.app, None, cmd_name='test')
self.hook = TestHook(self.cmd)
self.mgr = extension.ExtensionManager.make_test_instance(
[extension.Extension(
'parser-hook',
None,
None,
self.hook)],
)
# Replace the auto-loaded hooks with our explicitly created
# manager.
self.cmd._hooks = self.mgr
def test_get_parser(self):
parser = self.cmd.get_parser('test')
results = parser.parse_args(['--added-by-hook', 'value'])
self.assertEqual(results.added_by_hook, 'value')
def test_get_epilog(self):
results = self.cmd.get_epilog()
self.assertIn('hook epilog', results)
def test_before(self):
self.assertFalse(self.hook._before_called)
self.cmd.run(None)
self.assertTrue(self.hook._before_called)
def test_after(self):
self.assertFalse(self.hook._after_called)
self.cmd.run(None)
self.assertTrue(self.hook._after_called)
class TestShowOneHooks(base.TestBase):
def setUp(self):
super(TestShowOneHooks, self).setUp()
self.app = make_app()
self.cmd = TestShowCommand(self.app, None, cmd_name='test')
self.hook = TestHook(self.cmd)
self.mgr = extension.ExtensionManager.make_test_instance(
[extension.Extension(
'parser-hook',
None,
None,
self.hook)],
)
# Replace the auto-loaded hooks with our explicitly created
# manager.
self.cmd._hooks = self.mgr
def test_get_parser(self):
parser = self.cmd.get_parser('test')
results = parser.parse_args(['--added-by-hook', 'value'])
self.assertEqual(results.added_by_hook, 'value')
def test_get_epilog(self):
results = self.cmd.get_epilog()
self.assertIn('hook epilog', results)
def test_before(self):
self.assertFalse(self.hook._before_called)
parser = self.cmd.get_parser('test')
results = parser.parse_args(['--added-by-hook', 'value'])
self.cmd.run(results)
self.assertTrue(self.hook._before_called)
def test_after(self):
self.assertFalse(self.hook._after_called)
parser = self.cmd.get_parser('test')
results = parser.parse_args(['--added-by-hook', 'value'])
self.cmd.run(results)
self.assertTrue(self.hook._after_called)
class TestListerHooks(base.TestBase):
def setUp(self):
super(TestListerHooks, self).setUp()
self.app = make_app()
self.cmd = TestListerCommand(self.app, None, cmd_name='test')
self.hook = TestHook(self.cmd)
self.mgr = extension.ExtensionManager.make_test_instance(
[extension.Extension(
'parser-hook',
None,
None,
self.hook)],
)
# Replace the auto-loaded hooks with our explicitly created
# manager.
self.cmd._hooks = self.mgr
def test_get_parser(self):
parser = self.cmd.get_parser('test')
results = parser.parse_args(['--added-by-hook', 'value'])
self.assertEqual(results.added_by_hook, 'value')
def test_get_epilog(self):
results = self.cmd.get_epilog()
self.assertIn('hook epilog', results)
def test_before(self):
self.assertFalse(self.hook._before_called)
parser = self.cmd.get_parser('test')
results = parser.parse_args(['--added-by-hook', 'value'])
self.cmd.run(results)
self.assertTrue(self.hook._before_called)
def test_after(self):
self.assertFalse(self.hook._after_called)
parser = self.cmd.get_parser('test')
results = parser.parse_args(['--added-by-hook', 'value'])
self.cmd.run(results)
self.assertTrue(self.hook._after_called)

View File

@@ -1,152 +0,0 @@
# 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 mock
import testscenarios
from cliff import commandmanager
from cliff.tests import base
from cliff.tests import utils
load_tests = testscenarios.load_tests_apply_scenarios
class TestLookupAndFind(base.TestBase):
scenarios = [
('one-word', {'argv': ['one']}),
('two-words', {'argv': ['two', 'words']}),
('three-words', {'argv': ['three', 'word', 'command']}),
]
def test(self):
mgr = utils.TestCommandManager(utils.TEST_NAMESPACE)
cmd, name, remaining = mgr.find_command(self.argv)
self.assertTrue(cmd)
self.assertEqual(' '.join(self.argv), name)
self.assertFalse(remaining)
class TestLookupWithRemainder(base.TestBase):
scenarios = [
('one', {'argv': ['one', '--opt']}),
('two', {'argv': ['two', 'words', '--opt']}),
('three', {'argv': ['three', 'word', 'command', '--opt']}),
]
def test(self):
mgr = utils.TestCommandManager(utils.TEST_NAMESPACE)
cmd, name, remaining = mgr.find_command(self.argv)
self.assertTrue(cmd)
self.assertEqual(['--opt'], remaining)
class TestFindInvalidCommand(base.TestBase):
scenarios = [
('no-such-command', {'argv': ['a', '-b']}),
('no-command-given', {'argv': ['-b']}),
]
def test(self):
mgr = utils.TestCommandManager(utils.TEST_NAMESPACE)
try:
mgr.find_command(self.argv)
except ValueError as err:
# make sure err include 'a' when ['a', '-b']
self.assertIn(self.argv[0], str(err))
self.assertIn('-b', str(err))
else:
self.fail('expected a failure')
class TestFindUnknownCommand(base.TestBase):
def test(self):
mgr = utils.TestCommandManager(utils.TEST_NAMESPACE)
try:
mgr.find_command(['a', 'b'])
except ValueError as err:
self.assertIn("['a', 'b']", str(err))
else:
self.fail('expected a failure')
class TestDynamicCommands(base.TestBase):
def test_add(self):
mgr = utils.TestCommandManager(utils.TEST_NAMESPACE)
mock_cmd = mock.Mock()
mgr.add_command('mock', mock_cmd)
found_cmd, name, args = mgr.find_command(['mock'])
self.assertIs(mock_cmd, found_cmd)
def test_intersected_commands(self):
def foo(arg):
pass
def foo_bar():
pass
mgr = utils.TestCommandManager(utils.TEST_NAMESPACE)
mgr.add_command('foo', foo)
mgr.add_command('foo bar', foo_bar)
self.assertIs(foo_bar, mgr.find_command(['foo', 'bar'])[0])
self.assertIs(
foo,
mgr.find_command(['foo', 'arg0'])[0],
)
class TestLoad(base.TestBase):
def test_load_commands(self):
testcmd = mock.Mock(name='testcmd')
testcmd.name.replace.return_value = 'test'
mock_pkg_resources = mock.Mock(return_value=[testcmd])
with mock.patch('pkg_resources.iter_entry_points',
mock_pkg_resources) as iter_entry_points:
mgr = commandmanager.CommandManager('test')
iter_entry_points.assert_called_once_with('test')
names = [n for n, v in mgr]
self.assertEqual(['test'], names)
def test_load_commands_keep_underscores(self):
testcmd = mock.Mock()
testcmd.name = 'test_cmd'
mock_pkg_resources = mock.Mock(return_value=[testcmd])
with mock.patch('pkg_resources.iter_entry_points',
mock_pkg_resources) as iter_entry_points:
mgr = commandmanager.CommandManager(
'test',
convert_underscores=False,
)
iter_entry_points.assert_called_once_with('test')
names = [n for n, v in mgr]
self.assertEqual(['test_cmd'], names)
def test_load_commands_replace_underscores(self):
testcmd = mock.Mock()
testcmd.name = 'test_cmd'
mock_pkg_resources = mock.Mock(return_value=[testcmd])
with mock.patch('pkg_resources.iter_entry_points',
mock_pkg_resources) as iter_entry_points:
mgr = commandmanager.CommandManager(
'test',
convert_underscores=True,
)
iter_entry_points.assert_called_once_with('test')
names = [n for n, v in mgr]
self.assertEqual(['test cmd'], names)

View File

@@ -1,171 +0,0 @@
# 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.
"""Bash completion tests
"""
import mock
from cliff import app as application
from cliff import commandmanager
from cliff import complete
from cliff.tests import base
class TestCompletion(base.TestBase):
def test_dictionary(self):
sot = complete.CompleteDictionary()
sot.add_command("image delete".split(),
[mock.Mock(option_strings=["1"])])
sot.add_command("image list".split(),
[mock.Mock(option_strings=["2"])])
sot.add_command("image create".split(),
[mock.Mock(option_strings=["3"])])
sot.add_command("volume type create".split(),
[mock.Mock(option_strings=["4"])])
sot.add_command("volume type delete".split(),
[mock.Mock(option_strings=["5"])])
self.assertEqual("image volume", sot.get_commands())
result = sot.get_data()
self.assertEqual("image", result[0][0])
self.assertEqual("create delete list", result[0][1])
self.assertEqual("image_create", result[1][0])
self.assertEqual("3", result[1][1])
self.assertEqual("image_delete", result[2][0])
self.assertEqual("1", result[2][1])
self.assertEqual("image_list", result[3][0])
self.assertEqual("2", result[3][1])
def test_complete_dictionary_subcmd(self):
sot = complete.CompleteDictionary()
sot.add_command("image delete".split(),
[mock.Mock(option_strings=["1"])])
sot.add_command("image list".split(),
[mock.Mock(option_strings=["2"])])
sot.add_command("image list better".split(),
[mock.Mock(option_strings=["3"])])
self.assertEqual("image", sot.get_commands())
result = sot.get_data()
self.assertEqual("image", result[0][0])
self.assertEqual("delete list list_better", result[0][1])
self.assertEqual("image_delete", result[1][0])
self.assertEqual("1", result[1][1])
self.assertEqual("image_list", result[2][0])
self.assertEqual("2 better", result[2][1])
self.assertEqual("image_list_better", result[3][0])
self.assertEqual("3", result[3][1])
class FakeStdout:
def __init__(self):
self.content = []
def write(self, text):
self.content.append(text)
def make_string(self):
result = ''
for line in self.content:
result = result + line
return result
class TestCompletionAlternatives(base.TestBase):
def given_cmdo_data(self):
cmdo = "image server"
data = [("image", "create"),
("image_create", "--eolus"),
("server", "meta ssh"),
("server_meta_delete", "--wilson"),
("server_ssh", "--sunlight")]
return cmdo, data
def then_data(self, content):
self.assertIn(" cmds='image server'\n", content)
self.assertIn(" cmds_image='create'\n", content)
self.assertIn(" cmds_image_create='--eolus'\n", content)
self.assertIn(" cmds_server='meta ssh'\n", content)
self.assertIn(" cmds_server_meta_delete='--wilson'\n", content)
self.assertIn(" cmds_server_ssh='--sunlight'\n", content)
def test_complete_no_code(self):
output = FakeStdout()
sot = complete.CompleteNoCode("doesNotMatter", output)
sot.write(*self.given_cmdo_data())
self.then_data(output.content)
def test_complete_bash(self):
output = FakeStdout()
sot = complete.CompleteBash("openstack", output)
sot.write(*self.given_cmdo_data())
self.then_data(output.content)
self.assertIn("_openstack()\n", output.content[0])
self.assertIn("complete -F _openstack openstack\n", output.content[-1])
def test_complete_command_parser(self):
sot = complete.CompleteCommand(mock.Mock(), mock.Mock())
parser = sot.get_parser('nothing')
self.assertEqual("nothing", parser.prog)
self.assertEqual("print bash completion command\n ",
parser.description)
class TestCompletionAction(base.TestBase):
def given_complete_command(self):
cmd_mgr = commandmanager.CommandManager('cliff.tests')
app = application.App('testing', '1', cmd_mgr, stdout=FakeStdout())
sot = complete.CompleteCommand(app, mock.Mock())
cmd_mgr.add_command('complete', complete.CompleteCommand)
return sot, app, cmd_mgr
def then_actions_equal(self, actions):
optstr = ' '.join(opt for action in actions
for opt in action.option_strings)
self.assertEqual('-h --help --name --shell', optstr)
def test_complete_command_get_actions(self):
sot, app, cmd_mgr = self.given_complete_command()
app.interactive_mode = False
actions = sot.get_actions(["complete"])
self.then_actions_equal(actions)
def test_complete_command_get_actions_interactive(self):
sot, app, cmd_mgr = self.given_complete_command()
app.interactive_mode = True
actions = sot.get_actions(["complete"])
self.then_actions_equal(actions)
def test_complete_command_take_action(self):
sot, app, cmd_mgr = self.given_complete_command()
parsed_args = mock.Mock()
parsed_args.name = "test_take"
parsed_args.shell = "bash"
content = app.stdout.content
self.assertEqual(0, sot.take_action(parsed_args))
self.assertIn("_test_take()\n", content[0])
self.assertIn("complete -F _test_take test_take\n", content[-1])
self.assertIn(" cmds='complete help'\n", content)
self.assertIn(" cmds_complete='-h --help --name --shell'\n", content)
self.assertIn(" cmds_help='-h --help'\n", content)
def test_complete_command_remove_dashes(self):
sot, app, cmd_mgr = self.given_complete_command()
parsed_args = mock.Mock()
parsed_args.name = "test-take"
parsed_args.shell = "bash"
content = app.stdout.content
self.assertEqual(0, sot.take_action(parsed_args))
self.assertIn("_test_take()\n", content[0])
self.assertIn("complete -F _test_take test-take\n", content[-1])

View File

@@ -1,86 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# 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 argparse
import unittest
import mock
import six
from cliff.formatters import commaseparated
from cliff.tests import test_columns
class TestCSVFormatter(unittest.TestCase):
def test_commaseparated_list_formatter(self):
sf = commaseparated.CSVLister()
c = ('a', 'b', 'c')
d1 = ('A', 'B', 'C')
d2 = ('D', 'E', 'F')
data = [d1, d2]
expected = 'a,b,c\nA,B,C\nD,E,F\n'
output = six.StringIO()
parsed_args = mock.Mock()
parsed_args.quote_mode = 'none'
sf.emit_list(c, data, output, parsed_args)
actual = output.getvalue()
self.assertEqual(expected, actual)
def test_commaseparated_list_formatter_quoted(self):
sf = commaseparated.CSVLister()
c = ('a', 'b', 'c')
d1 = ('A', 'B', 'C')
d2 = ('D', 'E', 'F')
data = [d1, d2]
expected = '"a","b","c"\n"A","B","C"\n"D","E","F"\n'
output = six.StringIO()
# Parse arguments as if passed on the command-line
parser = argparse.ArgumentParser(description='Testing...')
sf.add_argument_group(parser)
parsed_args = parser.parse_args(['--quote', 'all'])
sf.emit_list(c, data, output, parsed_args)
actual = output.getvalue()
self.assertEqual(expected, actual)
def test_commaseparated_list_formatter_formattable_column(self):
sf = commaseparated.CSVLister()
c = ('a', 'b', 'c')
d1 = ('A', 'B', test_columns.FauxColumn(['the', 'value']))
data = [d1]
expected = 'a,b,c\nA,B,[\'the\'\\, \'value\']\n'
output = six.StringIO()
parsed_args = mock.Mock()
parsed_args.quote_mode = 'none'
sf.emit_list(c, data, output, parsed_args)
actual = output.getvalue()
self.assertEqual(expected, actual)
def test_commaseparated_list_formatter_unicode(self):
sf = commaseparated.CSVLister()
c = (u'a', u'b', u'c')
d1 = (u'A', u'B', u'C')
happy = u'高兴'
d2 = (u'D', u'E', happy)
data = [d1, d2]
expected = u'a,b,c\nA,B,C\nD,E,%s\n' % happy
output = six.StringIO()
parsed_args = mock.Mock()
parsed_args.quote_mode = 'none'
sf.emit_list(c, data, output, parsed_args)
actual = output.getvalue()
if six.PY2:
actual = actual.decode('utf-8')
self.assertEqual(expected, actual)

View File

@@ -1,129 +0,0 @@
#!/usr/bin/env python
#
# 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 json
from cliff.formatters import json_format
from cliff.tests import base
from cliff.tests import test_columns
import mock
import six
class TestJSONFormatter(base.TestBase):
def test_one(self):
sf = json_format.JSONFormatter()
c = ('a', 'b', 'c', 'd')
d = ('A', 'B', 'C', '"escape me"')
expected = {
'a': 'A',
'b': 'B',
'c': 'C',
'd': '"escape me"'
}
args = mock.Mock()
sf.add_argument_group(args)
args.noindent = True
output = six.StringIO()
sf.emit_one(c, d, output, args)
value = output.getvalue()
print(len(value.splitlines()))
self.assertEqual(1, len(value.splitlines()))
actual = json.loads(value)
self.assertEqual(expected, actual)
args.noindent = False
output = six.StringIO()
sf.emit_one(c, d, output, args)
value = output.getvalue()
self.assertEqual(6, len(value.splitlines()))
actual = json.loads(value)
self.assertEqual(expected, actual)
def test_formattablecolumn_one(self):
sf = json_format.JSONFormatter()
c = ('a', 'b', 'c', 'd')
d = ('A', 'B', 'C', test_columns.FauxColumn(['the', 'value']))
expected = {
'a': 'A',
'b': 'B',
'c': 'C',
'd': ['the', 'value'],
}
args = mock.Mock()
sf.add_argument_group(args)
args.noindent = True
output = six.StringIO()
sf.emit_one(c, d, output, args)
value = output.getvalue()
print(len(value.splitlines()))
self.assertEqual(1, len(value.splitlines()))
actual = json.loads(value)
self.assertEqual(expected, actual)
def test_list(self):
sf = json_format.JSONFormatter()
c = ('a', 'b', 'c')
d = (
('A1', 'B1', 'C1'),
('A2', 'B2', 'C2'),
('A3', 'B3', 'C3')
)
expected = [
{'a': 'A1', 'b': 'B1', 'c': 'C1'},
{'a': 'A2', 'b': 'B2', 'c': 'C2'},
{'a': 'A3', 'b': 'B3', 'c': 'C3'}
]
args = mock.Mock()
sf.add_argument_group(args)
args.noindent = True
output = six.StringIO()
sf.emit_list(c, d, output, args)
value = output.getvalue()
self.assertEqual(1, len(value.splitlines()))
actual = json.loads(value)
self.assertEqual(expected, actual)
args.noindent = False
output = six.StringIO()
sf.emit_list(c, d, output, args)
value = output.getvalue()
self.assertEqual(17, len(value.splitlines()))
actual = json.loads(value)
self.assertEqual(expected, actual)
def test_formattablecolumn_list(self):
sf = json_format.JSONFormatter()
c = ('a', 'b', 'c')
d = (
('A1', 'B1', test_columns.FauxColumn(['the', 'value'])),
)
expected = [
{'a': 'A1', 'b': 'B1', 'c': ['the', 'value']},
]
args = mock.Mock()
sf.add_argument_group(args)
args.noindent = True
output = six.StringIO()
sf.emit_list(c, d, output, args)
value = output.getvalue()
self.assertEqual(1, len(value.splitlines()))
actual = json.loads(value)
self.assertEqual(expected, actual)

View File

@@ -1,96 +0,0 @@
#!/usr/bin/env python
#
# 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 argparse
import six
from cliff.formatters import shell
from cliff.tests import base
from cliff.tests import test_columns
import mock
class TestShellFormatter(base.TestBase):
def test(self):
sf = shell.ShellFormatter()
c = ('a', 'b', 'c', 'd')
d = ('A', 'B', 'C', '"escape me"')
expected = 'a="A"\nb="B"\nd="\\"escape me\\""\n'
output = six.StringIO()
args = mock.Mock()
args.variables = ['a', 'b', 'd']
args.prefix = ''
sf.emit_one(c, d, output, args)
actual = output.getvalue()
self.assertEqual(expected, actual)
def test_args(self):
sf = shell.ShellFormatter()
c = ('a', 'b', 'c', 'd')
d = ('A', 'B', 'C', '"escape me"')
expected = 'Xd="\\"escape me\\""\n'
output = six.StringIO()
# Parse arguments as if passed on the command-line
parser = argparse.ArgumentParser(description='Testing...')
sf.add_argument_group(parser)
parsed_args = parser.parse_args(['--variable', 'd', '--prefix', 'X'])
sf.emit_one(c, d, output, parsed_args)
actual = output.getvalue()
self.assertEqual(expected, actual)
def test_formattable_column(self):
sf = shell.ShellFormatter()
c = ('a', 'b', 'c')
d = ('A', 'B', test_columns.FauxColumn(['the', 'value']))
expected = '\n'.join([
'a="A"',
'b="B"',
'c="[\'the\', \'value\']"\n',
])
output = six.StringIO()
args = mock.Mock()
args.variables = ['a', 'b', 'c']
args.prefix = ''
sf.emit_one(c, d, output, args)
actual = output.getvalue()
self.assertEqual(expected, actual)
def test_non_string_values(self):
sf = shell.ShellFormatter()
c = ('a', 'b', 'c', 'd', 'e')
d = (True, False, 100, '"esc"', six.text_type('"esc"'))
expected = ('a="True"\nb="False"\nc="100"\n'
'd="\\"esc\\""\ne="\\"esc\\""\n')
output = six.StringIO()
args = mock.Mock()
args.variables = ['a', 'b', 'c', 'd', 'e']
args.prefix = ''
sf.emit_one(c, d, output, args)
actual = output.getvalue()
self.assertEqual(expected, actual)
def test_non_bash_friendly_values(self):
sf = shell.ShellFormatter()
c = ('a', 'foo-bar', 'provider:network_type')
d = (True, 'baz', 'vxlan')
expected = 'a="True"\nfoo_bar="baz"\nprovider_network_type="vxlan"\n'
output = six.StringIO()
args = mock.Mock()
args.variables = ['a', 'foo-bar', 'provider:network_type']
args.prefix = ''
sf.emit_one(c, d, output, args)
actual = output.getvalue()
self.assertEqual(expected, actual)

View File

@@ -1,625 +0,0 @@
#!/usr/bin/env python
#
# 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 argparse
import os
import textwrap
import mock
from six import StringIO
from cliff.formatters import table
from cliff.tests import base
from cliff.tests import test_columns
class args(object):
def __init__(self, max_width=0, print_empty=False, fit_width=False):
self.fit_width = fit_width
if max_width > 0:
self.max_width = max_width
else:
# Envvar is only taken into account iff CLI parameter not given
self.max_width = int(os.environ.get('CLIFF_MAX_TERM_WIDTH', 0))
self.print_empty = print_empty
def _table_tester_helper(tags, data, extra_args=None):
"""Get table output as a string, formatted according to
CLI arguments, environment variables and terminal size
tags - tuple of strings for data tags (column headers or fields)
data - tuple of strings for single data row
- list of tuples of strings for multiple rows of data
extra_args - an instance of class args
- a list of strings for CLI arguments
"""
sf = table.TableFormatter()
if extra_args is None:
# Default to no CLI arguments
parsed_args = args()
elif type(extra_args) == args:
# Use the given CLI arguments
parsed_args = extra_args
else:
# Parse arguments as if passed on the command-line
parser = argparse.ArgumentParser(description='Testing...')
sf.add_argument_group(parser)
parsed_args = parser.parse_args(extra_args)
output = StringIO()
emitter = sf.emit_list if type(data) is list else sf.emit_one
emitter(tags, data, output, parsed_args)
return output.getvalue()
class TestTableFormatter(base.TestBase):
@mock.patch('cliff.utils.terminal_width')
def test(self, tw):
tw.return_value = 80
c = ('a', 'b', 'c', 'd')
d = ('A', 'B', 'C', 'test\rcarriage\r\nreturn')
expected = textwrap.dedent('''\
+-------+---------------+
| Field | Value |
+-------+---------------+
| a | A |
| b | B |
| c | C |
| d | test carriage |
| | return |
+-------+---------------+
''')
self.assertEqual(expected, _table_tester_helper(c, d))
class TestTerminalWidth(base.TestBase):
# Multi-line output when width is restricted to 42 columns
expected_ml_val = textwrap.dedent('''\
+-------+--------------------------------+
| Field | Value |
+-------+--------------------------------+
| a | A |
| b | B |
| c | C |
| d | dddddddddddddddddddddddddddddd |
| | dddddddddddddddddddddddddddddd |
| | ddddddddddddddddd |
+-------+--------------------------------+
''')
# Multi-line output when width is restricted to 80 columns
expected_ml_80_val = textwrap.dedent('''\
+-------+----------------------------------------------------------------------+
| Field | Value |
+-------+----------------------------------------------------------------------+
| a | A |
| b | B |
| c | C |
| d | dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd |
| | ddddddddd |
+-------+----------------------------------------------------------------------+
''') # noqa
# Single-line output, for when no line length restriction apply
expected_sl_val = textwrap.dedent('''\
+-------+-------------------------------------------------------------------------------+
| Field | Value |
+-------+-------------------------------------------------------------------------------+
| a | A |
| b | B |
| c | C |
| d | ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd |
+-------+-------------------------------------------------------------------------------+
''') # noqa
@mock.patch('cliff.utils.terminal_width')
def test_table_formatter_no_cli_param(self, tw):
tw.return_value = 80
c = ('a', 'b', 'c', 'd')
d = ('A', 'B', 'C', 'd' * 77)
self.assertEqual(
self.expected_ml_80_val,
_table_tester_helper(c, d, extra_args=args(fit_width=True)),
)
@mock.patch('cliff.utils.terminal_width')
def test_table_formatter_cli_param(self, tw):
tw.return_value = 80
c = ('a', 'b', 'c', 'd')
d = ('A', 'B', 'C', 'd' * 77)
self.assertEqual(
self.expected_ml_val,
_table_tester_helper(c, d, extra_args=['--max-width', '42']),
)
@mock.patch('cliff.utils.terminal_width')
def test_table_formatter_no_cli_param_unlimited_tw(self, tw):
tw.return_value = 0
c = ('a', 'b', 'c', 'd')
d = ('A', 'B', 'C', 'd' * 77)
# output should not be wrapped to multiple lines
self.assertEqual(
self.expected_sl_val,
_table_tester_helper(c, d, extra_args=args()),
)
@mock.patch('cliff.utils.terminal_width')
def test_table_formatter_cli_param_unlimited_tw(self, tw):
tw.return_value = 0
c = ('a', 'b', 'c', 'd')
d = ('A', 'B', 'C', 'd' * 77)
self.assertEqual(
self.expected_ml_val,
_table_tester_helper(c, d, extra_args=['--max-width', '42']),
)
@mock.patch('cliff.utils.terminal_width')
@mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '666'})
def test_table_formatter_cli_param_envvar_big(self, tw):
tw.return_value = 80
c = ('a', 'b', 'c', 'd')
d = ('A', 'B', 'C', 'd' * 77)
self.assertEqual(
self.expected_ml_val,
_table_tester_helper(c, d, extra_args=['--max-width', '42']),
)
@mock.patch('cliff.utils.terminal_width')
@mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '23'})
def test_table_formatter_cli_param_envvar_tiny(self, tw):
tw.return_value = 80
c = ('a', 'b', 'c', 'd')
d = ('A', 'B', 'C', 'd' * 77)
self.assertEqual(
self.expected_ml_val,
_table_tester_helper(c, d, extra_args=['--max-width', '42']),
)
class TestMaxWidth(base.TestBase):
expected_80 = textwrap.dedent('''\
+--------------------------+---------------------------------------------+
| Field | Value |
+--------------------------+---------------------------------------------+
| field_name | the value |
| a_really_long_field_name | a value significantly longer than the field |
+--------------------------+---------------------------------------------+
''')
@mock.patch('cliff.utils.terminal_width')
def test_80(self, tw):
tw.return_value = 80
c = ('field_name', 'a_really_long_field_name')
d = ('the value', 'a value significantly longer than the field')
self.assertEqual(self.expected_80, _table_tester_helper(c, d))
@mock.patch('cliff.utils.terminal_width')
def test_70(self, tw):
# resize value column
tw.return_value = 70
c = ('field_name', 'a_really_long_field_name')
d = ('the value', 'a value significantly longer than the field')
expected = textwrap.dedent('''\
+--------------------------+-----------------------------------------+
| Field | Value |
+--------------------------+-----------------------------------------+
| field_name | the value |
| a_really_long_field_name | a value significantly longer than the |
| | field |
+--------------------------+-----------------------------------------+
''')
self.assertEqual(
expected,
_table_tester_helper(c, d, extra_args=['--fit-width']),
)
@mock.patch('cliff.utils.terminal_width')
def test_50(self, tw):
# resize both columns
tw.return_value = 50
c = ('field_name', 'a_really_long_field_name')
d = ('the value', 'a value significantly longer than the field')
expected = textwrap.dedent('''\
+-----------------------+------------------------+
| Field | Value |
+-----------------------+------------------------+
| field_name | the value |
| a_really_long_field_n | a value significantly |
| ame | longer than the field |
+-----------------------+------------------------+
''')
self.assertEqual(
expected,
_table_tester_helper(c, d, extra_args=['--fit-width']),
)
@mock.patch('cliff.utils.terminal_width')
def test_10(self, tw):
# resize all columns limited by min_width=16
tw.return_value = 10
c = ('field_name', 'a_really_long_field_name')
d = ('the value', 'a value significantly longer than the field')
expected = textwrap.dedent('''\
+------------------+------------------+
| Field | Value |
+------------------+------------------+
| field_name | the value |
| a_really_long_fi | a value |
| eld_name | significantly |
| | longer than the |
| | field |
+------------------+------------------+
''')
self.assertEqual(
expected,
_table_tester_helper(c, d, extra_args=['--fit-width']),
)
class TestListFormatter(base.TestBase):
_col_names = ('one', 'two', 'three')
_col_data = [(
'one one one one one',
'two two two two',
'three three')]
_expected_mv = {
80: textwrap.dedent('''\
+---------------------+-----------------+-------------+
| one | two | three |
+---------------------+-----------------+-------------+
| one one one one one | two two two two | three three |
+---------------------+-----------------+-------------+
'''),
50: textwrap.dedent('''\
+----------------+-----------------+-------------+
| one | two | three |
+----------------+-----------------+-------------+
| one one one | two two two two | three three |
| one one | | |
+----------------+-----------------+-------------+
'''),
47: textwrap.dedent('''\
+---------------+---------------+-------------+
| one | two | three |
+---------------+---------------+-------------+
| one one one | two two two | three three |
| one one | two | |
+---------------+---------------+-------------+
'''),
45: textwrap.dedent('''\
+--------------+--------------+-------------+
| one | two | three |
+--------------+--------------+-------------+
| one one one | two two two | three three |
| one one | two | |
+--------------+--------------+-------------+
'''),
40: textwrap.dedent('''\
+------------+------------+------------+
| one | two | three |
+------------+------------+------------+
| one one | two two | three |
| one one | two two | three |
| one | | |
+------------+------------+------------+
'''),
10: textwrap.dedent('''\
+----------+----------+----------+
| one | two | three |
+----------+----------+----------+
| one one | two two | three |
| one one | two two | three |
| one | | |
+----------+----------+----------+
'''),
}
@mock.patch('cliff.utils.terminal_width')
def test_table_list_formatter(self, tw):
tw.return_value = 80
c = ('a', 'b', 'c')
d1 = ('A', 'B', 'C')
d2 = ('D', 'E', 'test\rcarriage\r\nreturn')
data = [d1, d2]
expected = textwrap.dedent('''\
+---+---+---------------+
| a | b | c |
+---+---+---------------+
| A | B | C |
| D | E | test carriage |
| | | return |
+---+---+---------------+
''')
self.assertEqual(expected, _table_tester_helper(c, data))
@mock.patch('cliff.utils.terminal_width')
def test_table_formatter_formattable_column(self, tw):
tw.return_value = 0
c = ('a', 'b', 'c', 'd')
d = ('A', 'B', 'C', test_columns.FauxColumn(['the', 'value']))
expected = textwrap.dedent('''\
+-------+---------------------------------------------+
| Field | Value |
+-------+---------------------------------------------+
| a | A |
| b | B |
| c | C |
| d | I made this string myself: ['the', 'value'] |
+-------+---------------------------------------------+
''')
self.assertEqual(expected, _table_tester_helper(c, d))
@mock.patch('cliff.utils.terminal_width')
def test_formattable_column(self, tw):
tw.return_value = 80
c = ('a', 'b', 'c')
d1 = ('A', 'B', test_columns.FauxColumn(['the', 'value']))
data = [d1]
expected = textwrap.dedent('''\
+---+---+---------------------------------------------+
| a | b | c |
+---+---+---------------------------------------------+
| A | B | I made this string myself: ['the', 'value'] |
+---+---+---------------------------------------------+
''')
self.assertEqual(expected, _table_tester_helper(c, data))
@mock.patch('cliff.utils.terminal_width')
def test_max_width_80(self, tw):
# no resize
l = tw.return_value = 80
self.assertEqual(
self._expected_mv[l],
_table_tester_helper(self._col_names, self._col_data),
)
@mock.patch('cliff.utils.terminal_width')
def test_max_width_50(self, tw):
# resize 1 column
l = tw.return_value = 50
actual = _table_tester_helper(self._col_names, self._col_data,
extra_args=['--fit-width'])
self.assertEqual(self._expected_mv[l], actual)
self.assertEqual(l, len(actual.splitlines()[0]))
@mock.patch('cliff.utils.terminal_width')
def test_max_width_45(self, tw):
# resize 2 columns
l = tw.return_value = 45
actual = _table_tester_helper(self._col_names, self._col_data,
extra_args=['--fit-width'])
self.assertEqual(self._expected_mv[l], actual)
self.assertEqual(l, len(actual.splitlines()[0]))
@mock.patch('cliff.utils.terminal_width')
def test_max_width_40(self, tw):
# resize all columns
l = tw.return_value = 40
actual = _table_tester_helper(self._col_names, self._col_data,
extra_args=['--fit-width'])
self.assertEqual(self._expected_mv[l], actual)
self.assertEqual(l, len(actual.splitlines()[0]))
@mock.patch('cliff.utils.terminal_width')
def test_max_width_10(self, tw):
# resize all columns limited by min_width=8
l = tw.return_value = 10
actual = _table_tester_helper(self._col_names, self._col_data,
extra_args=['--fit-width'])
self.assertEqual(self._expected_mv[l], actual)
# 3 columns each 8 wide, plus table spacing and borders
expected_width = 11 * 3 + 1
self.assertEqual(expected_width, len(actual.splitlines()[0]))
# Force a wide terminal by overriding its width with envvar
@mock.patch('cliff.utils.terminal_width')
@mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '666'})
def test_max_width_and_envvar_max(self, tw):
# no resize
tw.return_value = 80
self.assertEqual(
self._expected_mv[80],
_table_tester_helper(self._col_names, self._col_data),
)
# resize 1 column
tw.return_value = 50
self.assertEqual(
self._expected_mv[80],
_table_tester_helper(self._col_names, self._col_data),
)
# resize 2 columns
tw.return_value = 45
self.assertEqual(
self._expected_mv[80],
_table_tester_helper(self._col_names, self._col_data),
)
# resize all columns
tw.return_value = 40
self.assertEqual(
self._expected_mv[80],
_table_tester_helper(self._col_names, self._col_data),
)
# resize all columns limited by min_width=8
tw.return_value = 10
self.assertEqual(
self._expected_mv[80],
_table_tester_helper(self._col_names, self._col_data),
)
# Force a narrow terminal by overriding its width with envvar
@mock.patch('cliff.utils.terminal_width')
@mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '47'})
def test_max_width_and_envvar_mid(self, tw):
# no resize
tw.return_value = 80
self.assertEqual(
self._expected_mv[47],
_table_tester_helper(self._col_names, self._col_data),
)
# resize 1 column
tw.return_value = 50
actual = _table_tester_helper(self._col_names, self._col_data)
self.assertEqual(self._expected_mv[47], actual)
self.assertEqual(47, len(actual.splitlines()[0]))
# resize 2 columns
tw.return_value = 45
actual = _table_tester_helper(self._col_names, self._col_data)
self.assertEqual(self._expected_mv[47], actual)
self.assertEqual(47, len(actual.splitlines()[0]))
# resize all columns
tw.return_value = 40
actual = _table_tester_helper(self._col_names, self._col_data)
self.assertEqual(self._expected_mv[47], actual)
self.assertEqual(47, len(actual.splitlines()[0]))
# resize all columns limited by min_width=8
tw.return_value = 10
actual = _table_tester_helper(self._col_names, self._col_data)
self.assertEqual(self._expected_mv[47], actual)
self.assertEqual(47, len(actual.splitlines()[0]))
@mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '80'})
def test_env_maxwidth_noresize(self):
# no resize
self.assertEqual(
self._expected_mv[80],
_table_tester_helper(self._col_names, self._col_data),
)
@mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '50'})
def test_env_maxwidth_resize_one(self):
# resize 1 column
actual = _table_tester_helper(self._col_names, self._col_data)
self.assertEqual(self._expected_mv[50], actual)
self.assertEqual(50, len(actual.splitlines()[0]))
@mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '45'})
def test_env_maxwidth_resize_two(self):
# resize 2 columns
actual = _table_tester_helper(self._col_names, self._col_data)
self.assertEqual(self._expected_mv[45], actual)
self.assertEqual(45, len(actual.splitlines()[0]))
@mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '40'})
def test_env_maxwidth_resize_all(self):
# resize all columns
actual = _table_tester_helper(self._col_names, self._col_data)
self.assertEqual(self._expected_mv[40], actual)
self.assertEqual(40, len(actual.splitlines()[0]))
@mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '8'})
def test_env_maxwidth_resize_all_tiny(self):
# resize all columns limited by min_width=8
actual = _table_tester_helper(self._col_names, self._col_data)
self.assertEqual(self._expected_mv[10], actual)
# 3 columns each 8 wide, plus table spacing and borders
expected_width = 11 * 3 + 1
self.assertEqual(expected_width, len(actual.splitlines()[0]))
@mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '42'})
def test_env_maxwidth_args_big(self):
self.assertEqual(
self._expected_mv[80],
_table_tester_helper(self._col_names, self._col_data,
extra_args=args(666)),
)
@mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '42'})
def test_env_maxwidth_args_tiny(self):
self.assertEqual(
self._expected_mv[40],
_table_tester_helper(self._col_names, self._col_data,
extra_args=args(40)),
)
@mock.patch('cliff.utils.terminal_width')
def test_empty(self, tw):
tw.return_value = 80
c = ('a', 'b', 'c')
data = []
expected = '\n'
self.assertEqual(expected, _table_tester_helper(c, data))
@mock.patch('cliff.utils.terminal_width')
def test_empty_table(self, tw):
tw.return_value = 80
c = ('a', 'b', 'c')
data = []
expected = textwrap.dedent('''\
+---+---+---+
| a | b | c |
+---+---+---+
+---+---+---+
''')
self.assertEqual(
expected,
_table_tester_helper(c, data,
extra_args=['--print-empty']),
)
class TestFieldWidths(base.TestBase):
def test(self):
tf = table.TableFormatter
self.assertEqual(
{
'a': 1,
'b': 2,
'c': 3,
'd': 10
},
tf._field_widths(
('a', 'b', 'c', 'd'),
'+---+----+-----+------------+'),
)
def test_zero(self):
tf = table.TableFormatter
self.assertEqual(
{
'a': 0,
'b': 0,
'c': 0
},
tf._field_widths(
('a', 'b', 'c'),
'+--+-++'),
)
def test_info(self):
tf = table.TableFormatter
self.assertEqual((49, 4), (tf._width_info(80, 10)))
self.assertEqual((76, 76), (tf._width_info(80, 1)))
self.assertEqual((79, 0), (tf._width_info(80, 0)))
self.assertEqual((0, 0), (tf._width_info(0, 80)))

View File

@@ -1,65 +0,0 @@
#!/usr/bin/env python
#
# 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 six
from cliff.formatters import value
from cliff.tests import base
from cliff.tests import test_columns
class TestValueFormatter(base.TestBase):
def test(self):
sf = value.ValueFormatter()
c = ('a', 'b', 'c', 'd')
d = ('A', 'B', 'C', '"no escape me"')
expected = 'A\nB\nC\n"no escape me"\n'
output = six.StringIO()
sf.emit_one(c, d, output, None)
actual = output.getvalue()
self.assertEqual(expected, actual)
def test_formattable_column(self):
sf = value.ValueFormatter()
c = ('a', 'b', 'c', 'd')
d = ('A', 'B', 'C', test_columns.FauxColumn(['the', 'value']))
expected = "A\nB\nC\n['the', 'value']\n"
output = six.StringIO()
sf.emit_one(c, d, output, None)
actual = output.getvalue()
self.assertEqual(expected, actual)
def test_list_formatter(self):
sf = value.ValueFormatter()
c = ('a', 'b', 'c')
d1 = ('A', 'B', 'C')
d2 = ('D', 'E', 'F')
data = [d1, d2]
expected = 'A B C\nD E F\n'
output = six.StringIO()
sf.emit_list(c, data, output, None)
actual = output.getvalue()
self.assertEqual(expected, actual)
def test_list_formatter_formattable_column(self):
sf = value.ValueFormatter()
c = ('a', 'b', 'c')
d1 = ('A', 'B', test_columns.FauxColumn(['the', 'value']))
data = [d1]
expected = "A B ['the', 'value']\n"
output = six.StringIO()
sf.emit_list(c, data, output, None)
actual = output.getvalue()
self.assertEqual(expected, actual)

View File

@@ -1,100 +0,0 @@
#!/usr/bin/env python
#
# 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 six
import yaml
from cliff.formatters import yaml_format
from cliff.tests import base
from cliff.tests import test_columns
import mock
class TestYAMLFormatter(base.TestBase):
def test_format_one(self):
sf = yaml_format.YAMLFormatter()
c = ('a', 'b', 'c', 'd')
d = ('A', 'B', 'C', '"escape me"')
expected = {
'a': 'A',
'b': 'B',
'c': 'C',
'd': '"escape me"'
}
output = six.StringIO()
args = mock.Mock()
sf.emit_one(c, d, output, args)
actual = yaml.safe_load(output.getvalue())
self.assertEqual(expected, actual)
def test_formattablecolumn_one(self):
sf = yaml_format.YAMLFormatter()
c = ('a', 'b', 'c', 'd')
d = ('A', 'B', 'C', test_columns.FauxColumn(['the', 'value']))
expected = {
'a': 'A',
'b': 'B',
'c': 'C',
'd': ['the', 'value'],
}
args = mock.Mock()
sf.add_argument_group(args)
args.noindent = True
output = six.StringIO()
sf.emit_one(c, d, output, args)
value = output.getvalue()
print(len(value.splitlines()))
actual = yaml.safe_load(output.getvalue())
self.assertEqual(expected, actual)
def test_list(self):
sf = yaml_format.YAMLFormatter()
c = ('a', 'b', 'c')
d = (
('A1', 'B1', 'C1'),
('A2', 'B2', 'C2'),
('A3', 'B3', 'C3')
)
expected = [
{'a': 'A1', 'b': 'B1', 'c': 'C1'},
{'a': 'A2', 'b': 'B2', 'c': 'C2'},
{'a': 'A3', 'b': 'B3', 'c': 'C3'}
]
output = six.StringIO()
args = mock.Mock()
sf.add_argument_group(args)
sf.emit_list(c, d, output, args)
actual = yaml.safe_load(output.getvalue())
self.assertEqual(expected, actual)
def test_formattablecolumn_list(self):
sf = yaml_format.YAMLFormatter()
c = ('a', 'b', 'c')
d = (
('A1', 'B1', test_columns.FauxColumn(['the', 'value'])),
)
expected = [
{'a': 'A1', 'b': 'B1', 'c': ['the', 'value']},
]
args = mock.Mock()
sf.add_argument_group(args)
args.noindent = True
output = six.StringIO()
sf.emit_list(c, d, output, args)
actual = yaml.safe_load(output.getvalue())
self.assertEqual(expected, actual)

View File

@@ -1,174 +0,0 @@
# 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.
try:
from StringIO import StringIO
except:
from io import StringIO
import os
import sys
import mock
from cliff import app as application
from cliff import commandmanager
from cliff import help
from cliff.tests import base
from cliff.tests import utils
class TestHelp(base.TestBase):
def test_show_help_for_command(self):
# FIXME(dhellmann): Are commands tied too closely to the app? Or
# do commands know too much about apps by using them to get to the
# command manager?
stdout = StringIO()
app = application.App('testing', '1',
utils.TestCommandManager(utils.TEST_NAMESPACE),
stdout=stdout)
app.NAME = 'test'
help_cmd = help.HelpCommand(app, mock.Mock())
parser = help_cmd.get_parser('test')
parsed_args = parser.parse_args(['one'])
try:
help_cmd.run(parsed_args)
except SystemExit:
pass
self.assertEqual('TestParser', stdout.getvalue())
def test_list_matching_commands(self):
# FIXME(dhellmann): Are commands tied too closely to the app? Or
# do commands know too much about apps by using them to get to the
# command manager?
stdout = StringIO()
app = application.App('testing', '1',
utils.TestCommandManager(utils.TEST_NAMESPACE),
stdout=stdout)
app.NAME = 'test'
help_cmd = help.HelpCommand(app, mock.Mock())
parser = help_cmd.get_parser('test')
parsed_args = parser.parse_args(['t'])
try:
help_cmd.run(parsed_args)
except SystemExit:
pass
help_output = stdout.getvalue()
self.assertIn('Command "t" matches:', help_output)
self.assertIn('three word command\n two words\n', help_output)
def test_list_matching_commands_no_match(self):
# FIXME(dhellmann): Are commands tied too closely to the app? Or
# do commands know too much about apps by using them to get to the
# command manager?
stdout = StringIO()
app = application.App('testing', '1',
utils.TestCommandManager(utils.TEST_NAMESPACE),
stdout=stdout)
app.NAME = 'test'
help_cmd = help.HelpCommand(app, mock.Mock())
parser = help_cmd.get_parser('test')
parsed_args = parser.parse_args(['z'])
self.assertRaises(
ValueError,
help_cmd.run,
parsed_args,
)
def test_show_help_for_help(self):
# FIXME(dhellmann): Are commands tied too closely to the app? Or
# do commands know too much about apps by using them to get to the
# command manager?
stdout = StringIO()
app = application.App('testing', '1',
utils.TestCommandManager(utils.TEST_NAMESPACE),
stdout=stdout)
app.NAME = 'test'
app.options = mock.Mock()
help_cmd = help.HelpCommand(app, mock.Mock())
parser = help_cmd.get_parser('test')
parsed_args = parser.parse_args([])
try:
help_cmd.run(parsed_args)
except SystemExit:
pass
help_text = stdout.getvalue()
basecommand = os.path.split(sys.argv[0])[1]
self.assertIn('usage: %s [--version]' % basecommand, help_text)
self.assertIn('optional arguments:\n --version', help_text)
expected = (
' one Test command.\n'
' three word command Test command.\n'
)
self.assertIn(expected, help_text)
def test_list_deprecated_commands(self):
# FIXME(dhellmann): Are commands tied too closely to the app? Or
# do commands know too much about apps by using them to get to the
# command manager?
stdout = StringIO()
app = application.App('testing', '1',
utils.TestCommandManager(utils.TEST_NAMESPACE),
stdout=stdout)
app.NAME = 'test'
try:
app.run(['--help'])
except SystemExit:
pass
help_output = stdout.getvalue()
self.assertIn('two words', help_output)
self.assertIn('three word command', help_output)
self.assertNotIn('old cmd', help_output)
@mock.patch.object(commandmanager.EntryPointWrapper, 'load',
side_effect=Exception('Could not load EntryPoint'))
def test_show_help_with_ep_load_fail(self, mock_load):
stdout = StringIO()
app = application.App('testing', '1',
utils.TestCommandManager(utils.TEST_NAMESPACE),
stdout=stdout)
app.NAME = 'test'
app.options = mock.Mock()
app.options.debug = False
help_cmd = help.HelpCommand(app, mock.Mock())
parser = help_cmd.get_parser('test')
parsed_args = parser.parse_args([])
try:
help_cmd.run(parsed_args)
except SystemExit:
pass
help_output = stdout.getvalue()
self.assertIn('Commands:', help_output)
self.assertIn('Could not load', help_output)
self.assertNotIn('Exception: Could not load EntryPoint', help_output)
@mock.patch.object(commandmanager.EntryPointWrapper, 'load',
side_effect=Exception('Could not load EntryPoint'))
def test_show_help_print_exc_with_ep_load_fail(self, mock_load):
stdout = StringIO()
app = application.App('testing', '1',
utils.TestCommandManager(utils.TEST_NAMESPACE),
stdout=stdout)
app.NAME = 'test'
app.options = mock.Mock()
app.options.debug = True
help_cmd = help.HelpCommand(app, mock.Mock())
parser = help_cmd.get_parser('test')
parsed_args = parser.parse_args([])
try:
help_cmd.run(parsed_args)
except SystemExit:
pass
help_output = stdout.getvalue()
self.assertIn('Commands:', help_output)
self.assertIn('Could not load', help_output)
self.assertIn('Exception: Could not load EntryPoint', help_output)

View File

@@ -1,80 +0,0 @@
# -*- encoding: utf-8 -*-
#
# 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 cmd2
from cliff.interactive import InteractiveApp
from cliff.tests import base
class FakeApp(object):
NAME = 'Fake'
class TestInteractive(base.TestBase):
def make_interactive_app(self, *command_names):
fake_command_manager = [(x, None) for x in command_names]
return InteractiveApp(FakeApp, fake_command_manager,
stdin=None, stdout=None)
def _test_completenames(self, expecteds, prefix):
app = self.make_interactive_app('hips', 'hippo', 'nonmatching')
self.assertEqual(
set(app.completenames(prefix, '', 0, 1)), set(expecteds))
def test_cmd2_completenames(self):
# cmd2.Cmd define do_help method
self._test_completenames(['help'], 'he')
def test_cliff_completenames(self):
self._test_completenames(['hips', 'hippo'], 'hip')
def test_no_completenames(self):
self._test_completenames([], 'taz')
def test_both_completenames(self):
# cmd2.Cmd define do_history method
# NOTE(dtroyer): Before release 0.7.0 do_hi was also defined so we need
# to account for that in the list of possible responses.
# Remove this check after cmd2 0.7.0 is the minimum
# requirement.
if hasattr(cmd2.Cmd, "do_hi"):
self._test_completenames(['hi', 'history', 'hips', 'hippo'], 'hi')
else:
self._test_completenames(['history', 'hips', 'hippo'], 'hi')
def _test_completedefault(self, expecteds, line, begidx):
command_names = set(['show file', 'show folder', 'show long',
'list all'])
app = self.make_interactive_app(*command_names)
observeds = app.completedefault(None, line, begidx, None)
self.assertEqual(set(expecteds), set(observeds))
self.assertTrue(
set([line[:begidx] + x for x in observeds]) <= command_names
)
def test_empty_text_completedefault(self):
# line = 'show ' + begidx = 5 implies text = ''
self._test_completedefault(['file', 'folder', ' long'], 'show ', 5)
def test_nonempty_text_completedefault2(self):
# line = 'show f' + begidx = 6 implies text = 'f'
self._test_completedefault(['file', 'folder'], 'show f', 5)
def test_long_completedefault(self):
self._test_completedefault(['long'], 'show ', 6)
def test_no_completedefault(self):
self._test_completedefault([], 'taz ', 4)

View File

@@ -1,76 +0,0 @@
#!/usr/bin/env python
#
# 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 weakref
from cliff import lister
from cliff.tests import base
import mock
class FauxFormatter(object):
def __init__(self):
self.args = []
self.obj = weakref.proxy(self)
def emit_list(self, columns, data, stdout, args):
self.args.append((columns, data))
class ExerciseLister(lister.Lister):
def _load_formatter_plugins(self):
return {
'test': FauxFormatter(),
}
def take_action(self, parsed_args):
return (
parsed_args.columns,
[('a', 'A'), ('b', 'B')],
)
class TestLister(base.TestBase):
def test_formatter_args(self):
app = mock.Mock()
test_lister = ExerciseLister(app, [])
parsed_args = mock.Mock()
parsed_args.columns = ('Col1', 'Col2')
parsed_args.formatter = 'test'
test_lister.run(parsed_args)
f = test_lister._formatter_plugins['test']
self.assertEqual(1, len(f.args))
args = f.args[0]
self.assertEqual(list(parsed_args.columns), args[0])
data = list(args[1])
self.assertEqual([['a', 'A'], ['b', 'B']], data)
def test_no_exist_column(self):
test_lister = ExerciseLister(mock.Mock(), [])
parsed_args = mock.Mock()
parsed_args.columns = ('no_exist_column',)
parsed_args.formatter = 'test'
with mock.patch.object(test_lister, 'take_action') as mock_take_action:
mock_take_action.return_value = (('Col1', 'Col2', 'Col3'), [])
self.assertRaises(
ValueError,
test_lister.run,
parsed_args,
)

View File

@@ -1,84 +0,0 @@
#!/usr/bin/env python
#
# 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 weakref
from cliff import show
from cliff.tests import base
import mock
class FauxFormatter(object):
def __init__(self):
self.args = []
self.obj = weakref.proxy(self)
def emit_one(self, columns, data, stdout, args):
self.args.append((columns, data))
class ExerciseShowOne(show.ShowOne):
def _load_formatter_plugins(self):
return {
'test': FauxFormatter(),
}
def take_action(self, parsed_args):
return (
parsed_args.columns,
[('a', 'A'), ('b', 'B')],
)
class TestShow(base.TestBase):
def test_formatter_args(self):
app = mock.Mock()
test_show = ExerciseShowOne(app, [])
parsed_args = mock.Mock()
parsed_args.columns = ('Col1', 'Col2')
parsed_args.formatter = 'test'
test_show.run(parsed_args)
f = test_show._formatter_plugins['test']
self.assertEqual(1, len(f.args))
args = f.args[0]
self.assertEqual(list(parsed_args.columns), args[0])
data = list(args[1])
self.assertEqual([('a', 'A'), ('b', 'B')], data)
def test_dict2columns(self):
app = mock.Mock()
test_show = ExerciseShowOne(app, [])
d = {'a': 'A', 'b': 'B', 'c': 'C'}
expected = [('a', 'b', 'c'), ('A', 'B', 'C')]
actual = list(test_show.dict2columns(d))
self.assertEqual(expected, actual)
def test_no_exist_column(self):
test_show = ExerciseShowOne(mock.Mock(), [])
parsed_args = mock.Mock()
parsed_args.columns = ('no_exist_column',)
parsed_args.formatter = 'test'
with mock.patch.object(test_show, 'take_action') as mock_take_action:
mock_take_action.return_value = (('Col1', 'Col2', 'Col3'), [])
self.assertRaises(
ValueError,
test_show.run,
parsed_args,
)

View File

@@ -1,201 +0,0 @@
# Copyright (C) 2017, Red Hat, Inc.
#
# 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 argparse
import textwrap
from cliff import sphinxext
from cliff.tests import base
class TestSphinxExtension(base.TestBase):
def test_empty_help(self):
"""Handle positional and optional actions without help messages."""
parser = argparse.ArgumentParser(prog='hello-world', add_help=False)
parser.add_argument('name', action='store')
parser.add_argument('--language', dest='lang')
output = '\n'.join(sphinxext._format_parser(parser))
self.assertEqual(textwrap.dedent("""
.. program:: hello-world
.. code-block:: shell
hello-world [--language LANG] name
.. option:: --language <LANG>
.. option:: name
""").lstrip(), output)
def test_nonempty_help(self):
"""Handle positional and optional actions with help messages."""
parser = argparse.ArgumentParser(prog='hello-world', add_help=False)
parser.add_argument('name', help='user name')
parser.add_argument('--language', dest='lang',
help='greeting language')
output = '\n'.join(sphinxext._format_parser(parser))
self.assertEqual(textwrap.dedent("""
.. program:: hello-world
.. code-block:: shell
hello-world [--language LANG] name
.. option:: --language <LANG>
greeting language
.. option:: name
user name
""").lstrip(), output)
def test_description_epilog(self):
"""Handle a parser description, epilog."""
parser = argparse.ArgumentParser(prog='hello-world', add_help=False,
description='A "Hello, World" app.',
epilog='What am I doing down here?')
parser.add_argument('name', action='store')
parser.add_argument('--language', dest='lang')
output = '\n'.join(sphinxext._format_parser(parser))
self.assertEqual(textwrap.dedent("""
A "Hello, World" app.
.. program:: hello-world
.. code-block:: shell
hello-world [--language LANG] name
.. option:: --language <LANG>
.. option:: name
What am I doing down here?
""").lstrip(), output)
def test_flag(self):
"""Handle a boolean argparse action."""
parser = argparse.ArgumentParser(prog='hello-world', add_help=False)
parser.add_argument('name', help='user name')
parser.add_argument('--translate', action='store_true',
help='translate to local language')
output = '\n'.join(sphinxext._format_parser(parser))
self.assertEqual(textwrap.dedent("""
.. program:: hello-world
.. code-block:: shell
hello-world [--translate] name
.. option:: --translate
translate to local language
.. option:: name
user name
""").lstrip(), output)
def test_supressed(self):
"""Handle a supressed action."""
parser = argparse.ArgumentParser(prog='hello-world', add_help=False)
parser.add_argument('name', help='user name')
parser.add_argument('--variable', help=argparse.SUPPRESS)
output = '\n'.join(sphinxext._format_parser(parser))
self.assertEqual(textwrap.dedent("""
.. program:: hello-world
.. code-block:: shell
hello-world name
.. option:: name
user name
""").lstrip(), output)
def test_metavar(self):
"""Handle an option with a metavar."""
parser = argparse.ArgumentParser(prog='hello-world', add_help=False)
parser.add_argument('names', metavar='<NAME>', nargs='+',
help='a user name')
output = '\n'.join(sphinxext._format_parser(parser))
self.assertEqual(textwrap.dedent("""
.. program:: hello-world
.. code-block:: shell
hello-world <NAME> [<NAME> ...]
.. option:: NAME
a user name
""").lstrip(), output)
def test_multiple_opts(self):
"""Correctly output multiple opts on separate lines."""
parser = argparse.ArgumentParser(prog='hello-world', add_help=False)
parser.add_argument('name', help='user name')
parser.add_argument('--language', dest='lang',
help='greeting language')
parser.add_argument('--translate', action='store_true',
help='translate to local language')
parser.add_argument('--write-to-var-log-something-or-other',
action='store_true',
help='a long opt to force wrapping')
style_group = parser.add_mutually_exclusive_group(required=True)
style_group.add_argument('--polite', action='store_true',
help='use a polite greeting')
style_group.add_argument('--profane', action='store_true',
help='use a less polite greeting')
output = '\n'.join(sphinxext._format_parser(parser))
self.assertEqual(textwrap.dedent("""
.. program:: hello-world
.. code-block:: shell
hello-world
[--language LANG]
[--translate]
[--write-to-var-log-something-or-other]
(--polite | --profane)
name
.. option:: --language <LANG>
greeting language
.. option:: --translate
translate to local language
.. option:: --write-to-var-log-something-or-other
a long opt to force wrapping
.. option:: --polite
use a polite greeting
.. option:: --profane
use a less polite greeting
.. option:: name
user name
""").lstrip(), output)

View File

@@ -1,77 +0,0 @@
#!/usr/bin/env python
#
# 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 os
import struct
import sys
import unittest
import mock
from cliff import utils
from cliff.tests import base
class TestTerminalWidth(base.TestBase):
def test(self):
width = utils.terminal_width(sys.stdout)
# Results are specific to the execution environment, so only assert
# that no error is raised.
if width is not None:
self.assertIsInstance(width, int)
@unittest.skipIf(not hasattr(os, 'get_terminal_size'),
'only needed for python 3.3 onwards')
@mock.patch('cliff.utils.os')
def test_get_terminal_size(self, mock_os):
ts = os.terminal_size((10, 5))
mock_os.get_terminal_size.return_value = ts
width = utils.terminal_width(sys.stdout)
self.assertEqual(10, width)
mock_os.get_terminal_size.side_effect = OSError()
width = utils.terminal_width(sys.stdout)
self.assertIs(None, width)
@unittest.skipIf(hasattr(os, 'get_terminal_size'),
'only needed for python 3.2 and before')
@mock.patch('fcntl.ioctl')
def test_ioctl(self, mock_ioctl):
mock_ioctl.return_value = struct.pack('hhhh', 57, 101, 0, 0)
width = utils.terminal_width(sys.stdout)
self.assertEqual(101, width)
mock_ioctl.side_effect = IOError()
width = utils.terminal_width(sys.stdout)
self.assertIs(None, width)
@unittest.skipIf(hasattr(os, 'get_terminal_size'),
'only needed for python 3.2 and before')
@mock.patch('cliff.utils.ctypes')
@mock.patch('sys.platform', 'win32')
def test_windows(self, mock_ctypes):
mock_ctypes.create_string_buffer.return_value.raw = struct.pack(
'hhhhHhhhhhh', 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
mock_ctypes.windll.kernel32.GetStdHandle.return_value = -11
mock_ctypes.windll.kernel32.GetConsoleScreenBufferInfo.return_value = 1
width = utils.terminal_width(sys.stdout)
self.assertEqual(101, width)
mock_ctypes.windll.kernel32.GetConsoleScreenBufferInfo.return_value = 0
width = utils.terminal_width(sys.stdout)
self.assertIs(None, width)
width = utils.terminal_width('foo')
self.assertIs(None, width)

View File

@@ -1,48 +0,0 @@
# 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 cliff.command import Command
from cliff.commandmanager import CommandManager
TEST_NAMESPACE = 'cliff.test'
class TestParser(object):
def print_help(self, stdout):
stdout.write('TestParser')
class TestCommand(Command):
"Test command."
def get_parser(self, ignore):
# Make it look like this class is the parser
# so parse_args() is called.
return TestParser()
def take_action(self, args):
return
class TestDeprecatedCommand(TestCommand):
deprecated = True
class TestCommandManager(CommandManager):
def load_commands(self, namespace):
if namespace == TEST_NAMESPACE:
for key in ('one', 'two words', 'three word command'):
self.add_command(key, TestCommand)
self.add_command('old cmd', TestDeprecatedCommand)

View File

@@ -1,155 +0,0 @@
# 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 ctypes
import os
import struct
import sys
# Each edit operation is assigned different cost, such as:
# 'w' means swap operation, the cost is 0;
# 's' means substitution operation, the cost is 2;
# 'a' means insertion operation, the cost is 1;
# 'd' means deletion operation, the cost is 3;
# The smaller cost results in the better similarity.
COST = {'w': 0, 's': 2, 'a': 1, 'd': 3}
def damerau_levenshtein(s1, s2, cost):
"""Calculates the Damerau-Levenshtein distance between two strings.
The Levenshtein distance says the minimum number of single-character edits
(i.e. insertions, deletions, swap or substitution) required to change one
string to the other.
The idea is to reserve a matrix to hold the Levenshtein distances between
all prefixes of the first string and all prefixes of the second, then we
can compute the values in the matrix in a dynamic programming fashion. To
avoid a large space complexity, only the last three rows in the matrix is
needed.(row2 holds the current row, row1 holds the previous row, and row0
the row before that.)
More details:
https://en.wikipedia.org/wiki/Levenshtein_distance
https://github.com/git/git/commit/8af84dadb142f7321ff0ce8690385e99da8ede2f
"""
if s1 == s2:
return 0
len1 = len(s1)
len2 = len(s2)
if len1 == 0:
return len2 * cost['a']
if len2 == 0:
return len1 * cost['d']
row1 = [i * cost['a'] for i in range(len2 + 1)]
row2 = row1[:]
row0 = row1[:]
for i in range(len1):
row2[0] = (i + 1) * cost['d']
for j in range(len2):
# substitution
sub_cost = row1[j] + (s1[i] != s2[j]) * cost['s']
# insertion
ins_cost = row2[j] + cost['a']
# deletion
del_cost = row1[j + 1] + cost['d']
# swap
swp_condition = ((i > 0) and
(j > 0) and
(s1[i - 1] == s2[j]) and
(s1[i] == s2[j - 1])
)
# min cost
if swp_condition:
swp_cost = row0[j - 1] + cost['w']
p_cost = min(sub_cost, ins_cost, del_cost, swp_cost)
else:
p_cost = min(sub_cost, ins_cost, del_cost)
row2[j + 1] = p_cost
row0, row1, row2 = row1, row2, row0
return row1[-1]
def terminal_width(stdout):
if hasattr(os, 'get_terminal_size'):
# python 3.3 onwards has built-in support for getting terminal size
try:
return os.get_terminal_size().columns
except OSError:
return None
if sys.platform == 'win32':
return _get_terminal_width_windows(stdout)
else:
return _get_terminal_width_ioctl(stdout)
def _get_terminal_width_windows(stdout):
STD_INPUT_HANDLE = -10
STD_OUTPUT_HANDLE = -11
STD_ERROR_HANDLE = -12
std_to_win_handle = {
sys.stdin: STD_INPUT_HANDLE,
sys.stdout: STD_OUTPUT_HANDLE,
sys.stderr: STD_ERROR_HANDLE}
std_handle = std_to_win_handle.get(stdout)
if not std_handle:
return None
handle = ctypes.windll.kernel32.GetStdHandle(std_handle)
csbi = ctypes.create_string_buffer(22)
res = ctypes.windll.kernel32.GetConsoleScreenBufferInfo(handle, csbi)
if res:
(size_x, size_y, cur_pos_x, cur_pos_y, attr,
left, top, right, bottom, max_size_x, max_size_y) = struct.unpack(
"hhhhHhhhhhh", csbi.raw)
return size_x
def _get_terminal_width_ioctl(stdout):
from fcntl import ioctl
import termios
try:
# winsize structure has 4 unsigned short fields
winsize = b'\0' * struct.calcsize('hhhh')
try:
winsize = ioctl(stdout, termios.TIOCGWINSZ, winsize)
except IOError:
return None
except TypeError:
# this is raised in unit tests as stdout is sometimes a StringIO
return None
winsize = struct.unpack('hhhh', winsize)
columns = winsize[1]
if not columns:
return None
return columns
except IOError:
return None

View File

@@ -1,64 +0,0 @@
=================
Running demoapp
=================
Setup
-----
First, you need to create a virtual environment and activate it.
::
$ pip install virtualenv
$ virtualenv .venv
$ . .venv/bin/activate
(.venv)$
Next, install ``cliff`` in the environment.
::
(.venv)$ python setup.py install
Now, install the demo application into the virtual environment.
::
(.venv)$ cd demoapp
(.venv)$ python setup.py install
Usage
-----
With cliff and the demo setup up, you can now play with it.
To see a list of commands available, run::
(.venv)$ cliffdemo --help
One of the available commands is "simple" and running it
::
(.venv)$ cliffdemo simple
produces the following
::
sending greeting
hi!
To see help for an individual command, include the command name on the
command line::
(.venv)$ cliffdemo files --help
Cleaning Up
-----------
Finally, when done, deactivate your virtual environment::
(.venv)$ deactivate
$

View File

@@ -1,23 +0,0 @@
# -*- encoding: utf-8 -*-
import logging
from cliff.lister import Lister
class Encoding(Lister):
"""Show some unicode text
"""
log = logging.getLogger(__name__)
def take_action(self, parsed_args):
messages = [
u'pi: π',
u'GB18030:鼀丅㐀ٸཌྷᠧꌢ€',
]
return (
('UTF-8', 'Unicode'),
[(repr(t.encode('utf-8')), t)
for t in messages],
)

View File

@@ -1,50 +0,0 @@
# All Rights Reserved.
#
# 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 logging
from cliff.command import Command
from cliff.hooks import CommandHook
class Hooked(Command):
"A command to demonstrate how the hooks work"
log = logging.getLogger(__name__)
def take_action(self, parsed_args):
self.app.stdout.write('this command has an extension\n')
class Hook(CommandHook):
"""Hook sample for the 'hooked' command.
This would normally be provided by a separate package from the
main application, but is included in the demo app for simplicity.
"""
def get_parser(self, parser):
print('sample hook get_parser()')
parser.add_argument('--added-by-hook')
return parser
def get_epilog(self):
return 'extension epilog text'
def before(self, parsed_args):
self.cmd.app.stdout.write('before\n')
def after(self, parsed_args, return_code):
self.cmd.app.stdout.write('after\n')

View File

@@ -1,18 +0,0 @@
import logging
import os
from cliff.lister import Lister
class Files(Lister):
"""Show a list of files in the current directory.
The file name and size are printed by default.
"""
log = logging.getLogger(__name__)
def take_action(self, parsed_args):
return (('Name', 'Size'),
((n, os.stat(n).st_size) for n in os.listdir('.'))
)

View File

@@ -1,35 +0,0 @@
import sys
from cliff.app import App
from cliff.commandmanager import CommandManager
class DemoApp(App):
def __init__(self):
super(DemoApp, self).__init__(
description='cliff demo app',
version='0.1',
command_manager=CommandManager('cliff.demo'),
deferred_help=True,
)
def initialize_app(self, argv):
self.LOG.debug('initialize_app')
def prepare_to_run_command(self, cmd):
self.LOG.debug('prepare_to_run_command %s', cmd.__class__.__name__)
def clean_up(self, cmd, result, err):
self.LOG.debug('clean_up %s', cmd.__class__.__name__)
if err:
self.LOG.debug('got an error: %s', err)
def main(argv=sys.argv[1:]):
myapp = DemoApp()
return myapp.run(argv)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))

View File

@@ -1,31 +0,0 @@
import logging
import os
from cliff.show import ShowOne
class File(ShowOne):
"Show details about a file"
log = logging.getLogger(__name__)
def get_parser(self, prog_name):
parser = super(File, self).get_parser(prog_name)
parser.add_argument('filename', nargs='?', default='.')
return parser
def take_action(self, parsed_args):
stat_data = os.stat(parsed_args.filename)
columns = ('Name',
'Size',
'UID',
'GID',
'Modified Time',
)
data = (parsed_args.filename,
stat_data.st_size,
stat_data.st_uid,
stat_data.st_gid,
stat_data.st_mtime,
)
return (columns, data)

View File

@@ -1,24 +0,0 @@
import logging
from cliff.command import Command
class Simple(Command):
"A simple command that prints a message."
log = logging.getLogger(__name__)
def take_action(self, parsed_args):
self.log.info('sending greeting')
self.log.debug('debugging')
self.app.stdout.write('hi!\n')
class Error(Command):
"Always raises an error"
log = logging.getLogger(__name__)
def take_action(self, parsed_args):
self.log.info('causing error')
raise RuntimeError('this is the expected exception')

View File

@@ -1,71 +0,0 @@
#!/usr/bin/env python
PROJECT = 'cliffdemo'
# Change docs/sphinx/conf.py too!
VERSION = '0.1'
from setuptools import setup, find_packages
try:
long_description = open('README.rst', 'rt').read()
except IOError:
long_description = ''
setup(
name=PROJECT,
version=VERSION,
description='Demo app for cliff',
long_description=long_description,
author='Doug Hellmann',
author_email='doug.hellmann@gmail.com',
url='https://github.com/openstack/cliff',
download_url='https://github.com/openstack/cliff/tarball/master',
classifiers=['Development Status :: 3 - Alpha',
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Intended Audience :: Developers',
'Environment :: Console',
],
platforms=['Any'],
scripts=[],
provides=[],
install_requires=['cliff'],
namespace_packages=[],
packages=find_packages(),
include_package_data=True,
entry_points={
'console_scripts': [
'cliffdemo = cliffdemo.main:main'
],
'cliff.demo': [
'simple = cliffdemo.simple:Simple',
'two_part = cliffdemo.simple:Simple',
'error = cliffdemo.simple:Error',
'list files = cliffdemo.list:Files',
'files = cliffdemo.list:Files',
'file = cliffdemo.show:File',
'show file = cliffdemo.show:File',
'unicode = cliffdemo.encoding:Encoding',
'hooked = cliffdemo.hook:Hooked',
],
'cliff.demo.hooked': [
'sample-hook = cliffdemo.hook:Hook',
],
},
zip_safe=False,
)

View File

@@ -1,153 +0,0 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/cliff.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/cliff.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/cliff"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/cliff"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

@@ -1,262 +0,0 @@
# -*- coding: utf-8 -*-
#
# cliff documentation build configuration file, created by
# sphinx-quickstart on Wed Apr 25 11:14:29 2012.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import datetime
import subprocess
import openstackdocstheme
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'openstackdocstheme',
]
# openstackdocstheme options
repository_name = 'openstack/cliff'
bug_project = 'python-cliff'
bug_tag = ''
html_last_updated_fmt = '%Y-%m-%d %H:%M'
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'cliff'
copyright = u'2012-%s, Doug Hellmann' % datetime.datetime.today().year
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = subprocess.Popen(['cd ../..; python setup.py --version'],
shell=True, stdout=subprocess.PIPE).stdout.read()
version = version.strip()
# The full version, including alpha/beta/rc tags.
release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
# today = ''
# Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = []
# The reST default role (used for this markup: `text`) to use for all
# documents.
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
# html_theme = 'default'
html_theme = 'openstackdocs'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
html_theme_path = [openstackdocstheme.get_html_theme_path()]
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
# html_additional_pages = {}
# If false, no module index is generated.
# html_domain_indices = True
# If false, no index is generated.
# html_use_index = True
# If true, the index is split into individual pages for each letter.
# html_split_index = False
# If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'cliffdoc'
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author,
# documentclass [howto/manual]).
latex_documents = [
('index', 'cliff.tex', u'cliff Documentation',
u'Doug Hellmann', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False
# If true, show page references after internal links.
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
# latex_appendices = []
# If false, no module index is generated.
# latex_domain_indices = True
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'cliff', u'cliff Documentation',
[u'Doug Hellmann'], 1)
]
# If true, show URL addresses after external links.
# man_show_urls = False
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'cliff', u'cliff Documentation',
u'Doug Hellmann', 'cliff', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
# texinfo_appendices = []
# If false, no module index is generated.
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
# texinfo_show_urls = 'footnote'

View File

@@ -1,83 +0,0 @@
==================
For Contributors
==================
If you would like to contribute to cliff directly, these instructions
should help you get started. Bug reports, and feature requests are
all welcome through the `Launchpad project`_.
.. _Launchpad project: https://launchpad.net/python-cliff
Changes to cliff should be submitted for review via the Gerrit tool,
following the workflow documented at
http://docs.openstack.org/infra/manual/developers.html#development-workflow
Pull requests submitted through GitHub will be ignored.
Bugs should be filed under the `Launchpad project`_.
.. note::
Before contributing new features to clif core, please consider
whether they should be implemented as an extension instead. The
architecture is highly pluggable precisely to keep the core small.
Building Documentation
======================
The documentation for cliff is written in reStructuredText and
converted to HTML using Sphinx. The build itself is driven by make.
You will need the following packages in order to build the docs:
- Sphinx
- docutils
Once all of the tools are installed into a virtualenv using
pip, run ``make docs`` to generate the HTML version of the
documentation::
$ make docs
(cd docs && make clean html)
sphinx-build -b html -d build/doctrees source build/html
Running Sphinx v1.1.3
loading pickled environment... done
building [html]: targets for 1 source files that are out of date
updating environment: 1 added, 1 changed, 0 removed
reading sources... [100%] index
looking for now-outdated files... none found
pickling environment... done
done
preparing documents... done
writing output... [100%] index
writing additional files... genindex search
copying static files... done
dumping search index... done
dumping object inventory... done
build succeeded, 2 warnings.
Build finished. The HTML pages are in build/html.
The output version of the documentation ends up in
``./docs/build/html`` inside your sandbox.
Running Tests
=============
The test suite for clif uses tox_, which must be installed separately
(``pip install tox``).
To run the tests under Python 2.7 and 3.3 as well as PyPy, run ``tox``
from the top level directory of the git repository.
To run tests under a single version of Python, specify the appropriate
environment when running tox::
$ tox -e py27
Add new tests by modifying an existing file or creating new script in
the ``tests`` directory.
.. _tox: http://codespeak.net/tox
.. _developer-templates:

View File

@@ -1,22 +0,0 @@
=======================================================
cliff -- Command Line Interface Formulation Framework
=======================================================
cliff is a framework for building command line programs. It uses
plugins to define sub-commands, output formatters, and other
extensions.
.. toctree::
:maxdepth: 2
install/index
user/index
reference/index
contributors/index
.. rubric:: Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@@ -1,42 +0,0 @@
==============
Installation
==============
Python Versions
===============
cliff is being developed under Python 2.7 and tested with Python 3.5.
Dependencies
============
cliff depends on setuptools and its pkg_resources module.
.. _install-basic:
Basic Installation
==================
cliff should be installed into the same site-packages area where the
application and extensions are installed (either a virtualenv or the
global site-packages). You may need administrative privileges to do
that. The easiest way to install it is using pip_::
$ pip install cliff
or::
$ sudo pip install cliff
.. _pip: http://pypi.python.org/pypi/pip
Source Code
===========
The source is hosted on github: http://git.openstack.org/cgit/openstack/cliff
Reporting Bugs
==============
Please report bugs through the github project:
https://bugs.launchpad.net/python-cliff

View File

@@ -1,69 +0,0 @@
=======================
Cliff Class Reference
=======================
Application
===========
App
---
.. autoclass:: cliff.app.App
:members:
InteractiveApp
--------------
.. autoclass:: cliff.interactive.InteractiveApp
:members:
CommandManager
--------------
.. autoclass:: cliff.commandmanager.CommandManager
:members:
Command
-------
.. autoclass:: cliff.command.Command
:members:
CommandHook
-----------
.. autoclass:: cliff.hooks.CommandHook
:members:
ShowOne
-------
.. autoclass:: cliff.show.ShowOne
:members:
Lister
------
.. autoclass:: cliff.lister.Lister
:members:
Formatting Output
=================
Formatter
---------
.. autoclass:: cliff.formatters.base.Formatter
:members:
ListFormatter
-------------
.. autoclass:: cliff.formatters.base.ListFormatter
:members:
SingleFormatter
---------------
.. autoclass:: cliff.formatters.base.SingleFormatter
:members:

View File

@@ -1,45 +0,0 @@
====================
Command Completion
====================
A generic command completion command is available to generate a
bash-completion script. Currently, the command will generate a script
for bash versions 3 or 4. There is also a mode that generates only
data that can be used in your own script. The command completion script
is generated based on the commands and options that you have specified
in cliff.
Usage
=====
In order for your command to support command completions, you need to
add the `cliff.complete.CompleteCommand` class to your command manager.
::
self.command_manager.add_command('complete', cliff.complete.CompleteCommand)
When you run the command, it will generate a bash-completion script:
::
(.venv)$ mycmd complete
_mycmd()
{
local cur prev words
COMPREPLY=()
_get_comp_words_by_ref -n : cur prev words
# Command data:
cmds='agent aggregate backup'
cmds_agent='--name'
...
if [ -z "${completed}" ] ; then
COMPREPLY=( $( compgen -f -- "$cur" ) $( compgen -d -- "$cur" ) )
else
COMPREPLY=( $(compgen -W "${completed}" -- ${cur}) )
fi
return 0
}
complete -F _mycmd mycmd

View File

@@ -1,303 +0,0 @@
========================
Exploring the Demo App
========================
The cliff source package includes a ``demoapp`` directory containing
an example main program with several command plugins.
Setup
=====
To install and experiment with the demo app you should create a
virtual environment and activate it. This will make it easy to remove
the app later, since it doesn't do anything useful and you aren't
likely to want to hang onto it after you understand how it works.
::
$ pip install virtualenv
$ virtualenv .venv
$ . .venv/bin/activate
(.venv)$
Next, install cliff in the same environment.
::
(.venv)$ python setup.py install
Finally, install the demo application into the virtual environment.
::
(.venv)$ cd demoapp
(.venv)$ python setup.py install
Usage
=====
Both cliff and the demo installed, you can now run the command
``cliffdemo``.
For basic command usage instructions and a list of the commands
available from the plugins, run::
(.venv)$ cliffdemo -h
or::
(.venv)$ cliffdemo --help
Run the ``simple`` command by passing its name as argument to ``cliffdemo``.
::
(.venv)$ cliffdemo simple
The ``simple`` command prints this output to the console:
::
sending greeting
hi!
To see help for an individual command, use the ``help`` command::
(.venv)$ cliffdemo help files
or the ``--help`` option::
(.venv)$ cliffdemo files --help
The Source
==========
The ``cliffdemo`` application is defined in a ``cliffdemo`` package
containing several modules.
main.py
-------
The main application is defined in ``main.py``:
.. literalinclude:: ../../../demoapp/cliffdemo/main.py
:linenos:
The :class:`DemoApp` class inherits from :class:`App` and overrides
:func:`__init__` to set the program description and version number. It
also passes a :class:`CommandManager` instance configured to look for
plugins in the ``cliff.demo`` namespace.
The :func:`initialize_app` method of :class:`DemoApp` will be invoked
after the main program arguments are parsed, but before any command
processing is performed and before the application enters interactive
mode. This hook is intended for opening connections to remote web
services, databases, etc. using arguments passed to the main
application.
The :func:`prepare_to_run_command` method of :class:`DemoApp` will be
invoked after a command is identified, but before the command is given
its arguments and run. This hook is intended for pre-command
validation or setup that must be repeated and cannot be handled by
:func:`initialize_app`.
The :func:`clean_up` method of :class:`DemoApp` is invoked after a
command runs. If the command raised an exception, the exception object
is passed to :func:`clean_up`. Otherwise the ``err`` argument is
``None``.
The :func:`main` function defined in ``main.py`` is registered as a
console script entry point so that :class:`DemoApp` can be run from
the command line (see the discussion of ``setup.py`` below).
simple.py
---------
Two commands are defined in ``simple.py``:
.. literalinclude:: ../../../demoapp/cliffdemo/simple.py
:linenos:
:class:`Simple` demonstrates using logging to emit messages on the
console at different verbose levels.
::
(.venv)$ cliffdemo simple
sending greeting
hi!
(.venv)$ cliffdemo -v simple
prepare_to_run_command Simple
sending greeting
debugging
hi!
clean_up Simple
(.venv)$ cliffdemo -q simple
hi!
:class:`Error` always raises a :class:`RuntimeError` exception when it
is invoked, and can be used to experiment with the error handling
features of cliff.
::
(.venv)$ cliffdemo error
causing error
ERROR: this is the expected exception
(.venv)$ cliffdemo -v error
prepare_to_run_command Error
causing error
ERROR: this is the expected exception
clean_up Error
got an error: this is the expected exception
(.venv)$ cliffdemo --debug error
causing error
this is the expected exception
Traceback (most recent call last):
File ".../cliff/app.py", line 218, in run_subcommand
result = cmd.run(parsed_args)
File ".../cliff/command.py", line 43, in run
self.take_action(parsed_args)
File ".../demoapp/cliffdemo/simple.py", line 24, in take_action
raise RuntimeError('this is the expected exception')
RuntimeError: this is the expected exception
Traceback (most recent call last):
File "/Users/dhellmann/Envs/cliff/bin/cliffdemo", line 9, in <module>
load_entry_point('cliffdemo==0.1', 'console_scripts', 'cliffdemo')()
File ".../demoapp/cliffdemo/main.py", line 33, in main
return myapp.run(argv)
File ".../cliff/app.py", line 160, in run
result = self.run_subcommand(remainder)
File ".../cliff/app.py", line 218, in run_subcommand
result = cmd.run(parsed_args)
File ".../cliff/command.py", line 43, in run
self.take_action(parsed_args)
File ".../demoapp/cliffdemo/simple.py", line 24, in take_action
raise RuntimeError('this is the expected exception')
RuntimeError: this is the expected exception
.. _demoapp-list:
list.py
-------
``list.py`` includes a single command derived from
:class:`cliff.lister.Lister` which prints a list of the files in the
current directory.
.. literalinclude:: ../../../demoapp/cliffdemo/list.py
:linenos:
:class:`Files` prepares the data, and :class:`Lister` manages the
output formatter and printing the data to the console.
::
(.venv)$ cliffdemo files
+---------------+------+
| Name | Size |
+---------------+------+
| build | 136 |
| cliffdemo.log | 2546 |
| Makefile | 5569 |
| source | 408 |
+---------------+------+
(.venv)$ cliffdemo files -f csv
"Name","Size"
"build",136
"cliffdemo.log",2690
"Makefile",5569
"source",408
.. _demoapp-show:
show.py
-------
``show.py`` includes a single command derived from
:class:`cliff.show.ShowOne` which prints the properties of the named
file.
.. literalinclude:: ../../../demoapp/cliffdemo/show.py
:linenos:
:class:`File` prepares the data, and :class:`ShowOne` manages the
output formatter and printing the data to the console.
::
(.venv)$ cliffdemo file setup.py
+---------------+--------------+
| Field | Value |
+---------------+--------------+
| Name | setup.py |
| Size | 5825 |
| UID | 502 |
| GID | 20 |
| Modified Time | 1335569964.0 |
+---------------+--------------+
setup.py
--------
The demo application is packaged using setuptools.
.. literalinclude:: ../../../demoapp/setup.py
:linenos:
The important parts of the packaging instructions are the
``entry_points`` settings. All of the commands are registered in the
``cliff.demo`` namespace. Each main program should define its own
command namespace so that it only loads the command plugins that it
should be managing.
Command Extension Hooks
=======================
Individual subcommands of an application can be extended via hooks
registered as separate plugins. In the demo application, the
``hooked`` command has a single extension registered.
The namespace for hooks is a combination of the application namespace
and the command name. In this case, the application namespace is
``cliff.demo`` and the command is ``hooked``, so the extension
namespace is ``cliff.demo.hooked``. If the subcommand name includes
spaces, they are replaced with underscores ("``_``") to build the
namespace.
.. literalinclude:: ../../../demoapp/cliffdemo/hook.py
:linenos:
Although the ``hooked`` command does not add any arguments to the
parser it creates, the help output shows that the extension adds a
single ``--added-by-hook`` option.
::
(.venv)$ cliffdemo hooked -h
sample hook get_parser()
usage: cliffdemo hooked [-h] [--added-by-hook ADDED_BY_HOOK]
A command to demonstrate how the hooks work
optional arguments:
-h, --help show this help message and exit
--added-by-hook ADDED_BY_HOOK
extension epilog text
(.venv)$ cliffdemo hooked
sample hook get_parser()
before
this command has an extension
after
.. seealso::
:class:`cliff.hooks.CommandHook` -- The API for command hooks.

View File

@@ -1 +0,0 @@
.. include:: ../../../ChangeLog

View File

@@ -1,15 +0,0 @@
=============
Using cliff
=============
.. toctree::
:maxdepth: 2
introduction
demoapp
list_commands
show_commands
complete
interactive_mode
sphinxext
history

View File

@@ -1,92 +0,0 @@
==================
Interactive Mode
==================
In addition to running single commands from the command line, cliff
supports an interactive mode in which the user is presented with a
separate command shell. All of the command plugins available from the
command line are automatically configured as commands within the
shell.
Refer to the cmd2_ documentation for more details about features of
the shell.
.. _cmd2: http://packages.python.org/cmd2/index.html
Example
=======
The ``cliffdemo`` application enters interactive mode if no command is
specified on the command line.
::
(.venv)$ cliffdemo
(cliffdemo) help
Shell commands (type help <topic>):
===================================
cmdenvironment edit hi l list pause r save shell show
ed help history li load py run set shortcuts
Undocumented commands:
======================
EOF eof exit q quit
Application commands (type help <topic>):
=========================================
files help simple file error two part
To obtain instructions for a built-in or application command, use the
``help`` command:
::
(cliffdemo) help simple
usage: simple [-h]
A simple command that prints a message.
optional arguments:
-h, --help Show help message and exit.
The commands can be run, including options and arguments, as on the
regular command line:
::
(cliffdemo) simple
sending greeting
hi!
(cliffdemo) files
+----------------------+-------+
| Name | Size |
+----------------------+-------+
| .git | 578 |
| .gitignore | 268 |
| .tox | 238 |
| .venv | 204 |
| announce.rst | 1015 |
| announce.rst~ | 708 |
| cliff | 884 |
| cliff.egg-info | 340 |
| cliffdemo.log | 2193 |
| cliffdemo.log.1 | 10225 |
| demoapp | 408 |
| dist | 136 |
| distribute_setup.py | 15285 |
| distribute_setup.pyc | 15196 |
| docs | 238 |
| LICENSE | 11358 |
| Makefile | 376 |
| Makefile~ | 94 |
| MANIFEST.in | 186 |
| MANIFEST.in~ | 344 |
| README.rst | 1063 |
| setup.py | 5855 |
| setup.py~ | 8128 |
| tests | 204 |
| tox.ini | 76 |
| tox.ini~ | 421 |
+----------------------+-------+
(cliffdemo)

View File

@@ -1,76 +0,0 @@
==============
Introduction
==============
The cliff framework is meant to be used to create multi-level commands
such as subversion and git, where the main program handles some basic
argument parsing and then invokes a sub-command to do the work.
Command Plugins
===============
Cliff takes advantage of Python's ability to load code dynamically to
allow the sub-commands of a main program to be implemented, packaged,
and distributed separately from the main program. This organization
provides a unified view of the command for *users*, while giving
developers the opportunity organize source code in any way they see
fit.
Cliff Objects
=============
Cliff is organized around five objects that are combined to create a
useful command line program.
The Application
---------------
An :class:`cliff.app.App` is the main program that you run from the shell
command prompt. It is responsible for global operations that apply to
all of the commands, such as configuring logging and setting up I/O
streams.
The CommandManager
------------------
The :class:`cliff.commandmanager.CommandManager` knows how to load
individual command plugins. The default implementation uses
`setuptools entry points`_ but any mechanism for loading commands can
be used by replacing the default :class:`CommandManager` when
instantiating an :class:`App`.
The Command
-----------
The :class:`cliff.command.Command` class is where the real work
happens. The rest of the framework is present to help the user
discover the command plugins and invoke them, and to provide runtime
support for those plugins. Each :class:`Command` subclass is
responsible for taking action based on instructions from the user. It
defines its own local argument parser (usually using argparse_) and a
:func:`take_action` method that does the appropriate work.
The CommandHook
---------------
The :class:`cliff.hooks.CommandHook` class can extend a Command by
modifying the command line arguments available, for example to add
options used by a driver. Each CommandHook subclass must implement the
full hook API, defined by the base class. Extensions should be
registered using an entry point namespace based on the application
namespace and the command name::
application_namespace + '.' + command_name.replace(' ', '_')
The Interactive Application
---------------------------
The main program uses an :class:`cliff.interactive.InteractiveApp`
instance to provide a command-shell mode in which the user can type
multiple commands before the program exits. Many cliff-based
applications will be able to use the default implementation of
:class:`InteractiveApp` without subclassing it.
.. _setuptools entry points: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points
.. _argparse: http://docs.python.org/library/argparse.html

View File

@@ -1,158 +0,0 @@
===============
List Commands
===============
One of the most common patterns with command line programs is the need
to print lists of data. cliff provides a base class for commands of
this type so that they only need to prepare the data, and the user can
choose from one of several output formatter plugins to see the list of
data in their preferred format.
Lister
======
The :class:`cliff.lister.Lister` base class API extends
:class:`Command` to allow :func:`take_action` to return data to be
formatted using a user-selectable formatter. Subclasses should provide
a :func:`take_action` implementation that returns a two member tuple
containing a tuple with the names of the columns in the dataset and an
iterable that will yield the data to be output. See the description of
:ref:`the files command in the demoapp <demoapp-list>` for details.
List Output Formatters
======================
cliff is delivered with two output formatters for list
commands. :class:`Lister` adds a command line switch to let the user
specify the formatter they want, so you don't have to do any extra
work in your application.
csv
---
The ``csv`` formatter produces a comma-separated-values document as
output. CSV data can be imported into a database or spreadsheet for
further manipulation.
::
(.venv)$ cliffdemo files -f csv
"Name","Size"
"build",136
"cliffdemo.log",2690
"Makefile",5569
"source",408
table
-----
The ``table`` formatter uses PrettyTable_ to produce output formatted
for human consumption.
.. _PrettyTable: http://code.google.com/p/prettytable/
::
(.venv)$ cliffdemo files
+---------------+------+
| Name | Size |
+---------------+------+
| build | 136 |
| cliffdemo.log | 2546 |
| Makefile | 5569 |
| source | 408 |
+---------------+------+
value
-----
The ``value`` formatter produces a space separated output with no headers.
::
(.venv)$ cliffdemo files -f value
build 136
cliffdemo.log 2690
Makefile 5569
source 408
This format can be very convenient when you want to pipe the output to
a script.
::
(.venv)$ cliffdemo files -f value | while read NAME SIZE
do
echo $NAME is $SIZE bytes
done
build is 136 bytes
cliffdemo.log is 2690 bytes
Makefile is 5569 bytes
source is 408 bytes
yaml
----
The ``yaml`` formatter uses PyYAML_ to produce a YAML sequence of
mappings.
.. _PyYAML: http://pyyaml.org/
::
(.venv)$ cliffdemo files -f yaml
- Name: dist
Size: 4096
- Name: cliffdemo.egg-info
Size: 4096
- Name: README.rst
Size: 960
- Name: setup.py
Size: 1807
- Name: build
Size: 4096
- Name: cliffdemo
Size: 4096
json
----
The ``json`` formatter produces an array of objects in indented JSON
format.
::
(.venv)$ cliffdemo files -f json
[
{
"Name": "source",
"Size": 4096
},
{
"Name": "Makefile",
"Size": 5569
},
{
"Name": "build",
"Size": 4096
}
]
Other Formatters
----------------
A formatter using tablib_ to produce HTML is available as part of
`cliff-tablib`_.
.. _cliff-tablib: https://github.com/dreamhost/cliff-tablib
Creating Your Own Formatter
---------------------------
If the standard formatters do not meet your needs, you can bundle
another formatter with your program by subclassing from
:class:`cliff.formatters.base.ListFormatter` and registering the
plugin in the ``cliff.formatter.list`` namespace.
.. _tablib: https://github.com/kennethreitz/tablib

View File

@@ -1,137 +0,0 @@
===============
Show Commands
===============
One of the most common patterns with command line programs is the need
to print properties of objects. cliff provides a base class for
commands of this type so that they only need to prepare the data, and
the user can choose from one of several output formatter plugins to
see the data in their preferred format.
ShowOne
=======
The :class:`cliff.show.ShowOne` base class API extends
:class:`Command` to allow :func:`take_action` to return data to be
formatted using a user-selectable formatter. Subclasses should provide
a :func:`take_action` implementation that returns a two member tuple
containing a tuple with the names of the columns in the dataset and an
iterable that contains the data values associated with those
names. See the description of :ref:`the file command in the demoapp
<demoapp-show>` for details.
Show Output Formatters
======================
cliff is delivered with output formatters for show
commands. :class:`ShowOne` adds a command line switch to let the user
specify the formatter they want, so you don't have to do any extra
work in your application.
table
-----
The ``table`` formatter uses PrettyTable_ to produce output
formatted for human consumption. This is the default formatter.
.. _PrettyTable: http://code.google.com/p/prettytable/
::
(.venv)$ cliffdemo file setup.py
+---------------+--------------+
| Field | Value |
+---------------+--------------+
| Name | setup.py |
| Size | 5825 |
| UID | 502 |
| GID | 20 |
| Modified Time | 1335569964.0 |
+---------------+--------------+
shell
-----
The ``shell`` formatter produces output that can be parsed directly by
a typical UNIX shell as variable assignments. This avoids extra
parsing overhead in shell scripts.
::
(.venv)$ cliffdemo file -f shell setup.py
name="setup.py"
size="5916"
uid="527"
gid="501"
modified_time="1335655655.0"
(.venv)$ eval "$(cliffdemo file -f shell --prefix example_ setup.py)"
(.venv)$ echo $example_size
5916
value
-----
The ``value`` formatter produces output that only contains the
value of the field or fields.
::
(.venv)$ cliffdemo file -f value -c Size setup.py
5916
(.venv)$ SIZE="$(cliffdemo file -f value -c Size setup.py)"
(.venv)$ echo $SIZE
5916
yaml
----
The ``yaml`` formatter uses PyYAML_ to produce a YAML mapping where
the field name is the key.
.. _PyYAML: http://pyyaml.org/
::
(.venv)$ cliffdemo file -f yaml setup.py
Name: setup.py
Size: 1807
UID: 1000
GID: 1000
Modified Time: 1393531476.9587486
json
----
The ``json`` formatter produces a JSON object where the field name
is the key.
::
(.venv)$ cliffdemo file -f json setup.py
{
"Modified Time": 1438726433.8055942,
"GID": 1000,
"UID": 1000,
"Name": "setup.py",
"Size": 1028
}
Other Formatters
----------------
A formatter using tablib_ to produce HTML is available as part of
`cliff-tablib`_.
.. _cliff-tablib: https://github.com/dreamhost/cliff-tablib
Creating Your Own Formatter
---------------------------
If the standard formatters do not meet your needs, you can bundle
another formatter with your program by subclassing from
:class:`cliff.formatters.base.ShowFormatter` and registering the
plugin in the ``cliff.formatter.show`` namespace.
.. _tablib: https://github.com/kennethreitz/tablib

View File

@@ -1,149 +0,0 @@
==================
Sphinx Integration
==================
cliff supports integration with Sphinx by way of a `Sphinx directives`__.
.. rst:directive:: .. autoprogram-cliff:: namespace
Automatically document an instance of :py:class:`cliff.command.Command`,
including a description, usage summary, and overview of all options.
.. code-block:: rst
.. autoprogram-cliff:: openstack.compute.v2
:command: server add fixed ip
One argument is required, corresponding to the namespace that the command(s)
can be found in. This is generally defined in the `entry_points` section of
either `setup.cfg` or `setup.py`. Refer to the example_ below for more
information.
In addition, the following directive options can be supplied:
`:command:`
The name of the command, as it would appear if called from the command
line without the executable name. This will be defined in `setup.cfg` or
`setup.py` albeit with underscores. This is optional and `fnmatch-style`__
wildcarding is supported. Refer to the example_ below for more
information.
`:application:`
The top-level application name, which will be prefixed before all
commands. This option overrides the global option
`autoprogram_cliff_application` described below.
In most cases the global configuration is enough, but this option is
useful if your sphinx document handles multiple cliff applications.
.. seealso:: The ``autoprogram_cliff_application`` configuration option.
`:ignored:`
A comma-separated list of options to exclude from documentation for this
option. This is useful for options that are of low value.
.. seealso:: The ``autoprogram_cliff_ignored`` configuration option.
The following global configuration values are supported. These should be
placed in `conf.py`:
`autoprogram_cliff_application`
The top-level application name, which will be prefixed before all
commands. This is generally defined in the `console_scripts` attribute of
the `entry_points` section of either `setup.cfg` or `setup.py`. Refer to
the example_ below for more information.
For example:
.. code-block:: python
autoprogram_cliff_application = 'my-sample-application'
Defaults to ``''``
.. seealso:: The ``:command:`` directive option.
.. seealso:: The ``:application:`` directive option.
`autoprogram_cliff_ignored`
A global list of options to exclude from documentation. This can be used
to prevent duplication of common options, such as those used for
pagination, across **all** options.
For example:
.. code-block:: python
autoprogram_cliff_ignored = ['--help', '--page', '--order']
Defaults to ``['--help']``
.. seealso:: The ``:ignored:`` directive option.
.. seealso::
Module `sphinxcontrib.autoprogram`
An equivalent library for use with plain-old `argparse` applications.
Module `sphinx-click`
An equivalent library for use with `click` applications.
.. important::
The :rst:dir:`autoprogram-cliff` directive emits :rst:dir:`code-block`
snippets marked up as `shell` code. This requires `pygments` >= 0.6.
.. _example:
Example
=======
Take a sample `setup.cfg` file, which is based on the `setup.cfg` for the
`python-openstackclient` project:
.. code-block:: ini
[entry_points]
console_scripts =
openstack = openstackclient.shell:main
openstack.compute.v2 =
host_list = openstackclient.compute.v2.host:ListHost
host_set = openstackclient.compute.v2.host:SetHost
host_show = openstackclient.compute.v2.host:ShowHost
This will register three commands - ``host list``, ``host set`` and ``host
show`` - for a top-level executable called ``openstack``. To document the first
of these, add the following:
.. code-block:: rst
.. autoprogram-cliff:: openstack.compute.v2
:command: host list
You could also register all of these at once like so:
.. code-block:: rst
.. autoprogram-cliff:: openstack.compute.v2
:command: host *
Finally, if these are the only commands available in that namespace, you can
omit the `:command:` parameter entirely:
.. code-block:: rst
.. autoprogram-cliff:: openstack.compute.v2
In all cases, you should add the following to your `conf.py` to ensure all
usage examples show the full command name:
.. code-block:: python
autoprogram_cliff_application = 'openstack'
__ http://www.sphinx-doc.org/en/stable/extdev/markupapi.html
__ https://docs.python.org/3/library/fnmatch.html

View File

@@ -1,17 +0,0 @@
#!/bin/sh -x
set -e
envdir=$1
# The source for the client library is checked out by pip because of
# the deps listed in tox.ini, so we just need to move into that
# directory.
# NOTE(tonyb): tools/tox_install.sh will place the code in 1 of 2 paths
# depending on whether zuul-cloner is used, so try each possible location
cd $envdir/src/python-neutronclient || \
cd $envdir/src/openstack/python-neutronclient
pip install -r test-requirements.txt
python setup.py testr

View File

@@ -1,21 +0,0 @@
#!/bin/sh -x
set -e
envdir=$1
# The source for the client library is checked out by pip because of
# the deps listed in tox.ini, so we just need to move into that
# directory.
# NOTE(tonyb): tools/tox_install.sh will place the code in 1 of 2 paths
# depending on whether zuul-cloner is used, so try each possible location
cd $envdir/src/python-openstackclient/ || \
cd $envdir/src/openstack/python-openstackclient/
pip install -r test-requirements.txt
# Force a known hash seed value to avoid sorting errors from tox
# giving us a random one.
export PYTHONHASHSEED=0
python setup.py testr

View File

View File

@@ -1,11 +0,0 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
pbr!=2.1.0,>=2.0.0 # Apache-2.0
cmd2>=0.6.7 # MIT
PrettyTable<0.8,>=0.7.1 # BSD
pyparsing>=2.1.0 # MIT
six>=1.9.0 # MIT
stevedore>=1.20.0 # Apache-2.0
unicodecsv>=0.8.0;python_version<'3.0' # BSD
PyYAML>=3.10.0 # MIT

View File

@@ -1,51 +0,0 @@
[metadata]
name = cliff
description-file = README.rst
author = OpenStack
author-email = openstack-dev@lists.openstack.org
summary = Command Line Interface Formulation Framework
home-page = http://docs.openstack.org/developer/cliff
classifier =
Development Status :: 5 - Production/Stable
License :: OSI Approved :: Apache Software License
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
Intended Audience :: Developers
Environment :: Console
[global]
setup-hooks =
pbr.hooks.setup_hook
[files]
packages =
cliff
[entry_points]
cliff.formatter.list =
table = cliff.formatters.table:TableFormatter
csv = cliff.formatters.commaseparated:CSVLister
value = cliff.formatters.value:ValueFormatter
yaml = cliff.formatters.yaml_format:YAMLFormatter
json = cliff.formatters.json_format:JSONFormatter
cliff.formatter.show =
table = cliff.formatters.table:TableFormatter
shell = cliff.formatters.shell:ShellFormatter
value = cliff.formatters.value:ValueFormatter
yaml = cliff.formatters.yaml_format:YAMLFormatter
json = cliff.formatters.json_format:JSONFormatter
cliff.formatter.completion =
bash = cliff.complete:CompleteBash
none = cliff.complete:CompleteNoCode
[build_sphinx]
all-files = 1
warning-is-error = 1
build-dir = doc/build
source-dir = doc/source

View File

@@ -1,29 +0,0 @@
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# 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 FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
# In python < 2.7.4, a lazy loading of package `pbr` will break
# setuptools if some other modules registered functions in `atexit`.
# solution from: http://bugs.python.org/issue15881#msg170215
try:
import multiprocessing # noqa
except ImportError:
pass
setuptools.setup(
setup_requires=['pbr>=2.0.0'],
pbr=True)

View File

@@ -1,15 +0,0 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
python-subunit>=0.0.18 # Apache-2.0/BSD
testrepository>=0.0.18 # Apache-2.0/BSD
testtools>=1.4.0 # MIT
mock>=2.0 # BSD
testscenarios>=0.4 # Apache-2.0/BSD
coverage!=4.4,>=4.0 # Apache-2.0
# this is required for the docs build jobs
sphinx>=1.6.2 # BSD
openstackdocstheme>=1.11.0 # Apache-2.0

View File

@@ -1,85 +0,0 @@
#!/usr/bin/env bash
# Client constraint file contains this client version pin that is in conflict
# with installing the client from source. We should remove the version pin in
# the constraints file before applying it for from-source installation.
# The script also has a secondary purpose to install certain special
# dependencies directly from git.
# Wrapper for pip install that always uses constraints.
function pip_install() {
pip install -c"$localfile" -U "$@"
}
# Grab the library from git using either zuul-cloner or pip. The former is
# there to a take advantage of the setup done by the gate infrastructure
# and honour any/all Depends-On headers in the commit message
function install_from_git() {
ZUUL_CLONER=/usr/zuul-env/bin/zuul-cloner
GIT_HOST=git.openstack.org
PROJ=$1
EGG=$2
edit-constraints "$localfile" -- "$EGG"
if [ -x "$ZUUL_CLONER" ]; then
SRC_DIR="$VIRTUAL_ENV/src"
mkdir -p "$SRC_DIR"
cd "$SRC_DIR" >/dev/null
ZUUL_CACHE_DIR=${ZUUL_CACHE_DIR:-/opt/git} $ZUUL_CLONER \
--branch "$BRANCH_NAME" \
"git://$GIT_HOST" "$PROJ"
pip_install -e "$PROJ/."
cd - >/dev/null
else
pip_install -e"git+https://$GIT_HOST/$PROJ@$BRANCH_NAME#egg=${EGG}"
fi
}
CONSTRAINTS_FILE="$1"
shift 1
# This script will either complete with a return code of 0 or the return code
# of whatever failed.
set -e
# NOTE(tonyb): Place this in the tox environment's log dir so it will get
# published to logs.openstack.org for easy debugging.
mkdir -p "$VIRTUAL_ENV/log/"
localfile="$VIRTUAL_ENV/log/upper-constraints.txt"
if [[ "$CONSTRAINTS_FILE" != http* ]]; then
CONSTRAINTS_FILE="file://$CONSTRAINTS_FILE"
fi
# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep
curl "$CONSTRAINTS_FILE" --insecure --progress-bar --output "$localfile"
pip_install openstack-requirements
# This is the main purpose of the script: Allow local installation of
# the current repo. It is listed in constraints file and thus any
# install will be constrained and we need to unconstrain it.
edit-constraints "$localfile" -- "$CLIENT_NAME"
declare -a passthrough_args
while [ $# -gt 0 ] ; do
case "$1" in
# If we have any special os:<repo_name:<egg_name> deps then process them
os:*)
declare -a pkg_spec
IFS=: pkg_spec=($1)
install_from_git "${pkg_spec[1]}" "${pkg_spec[2]}"
;;
# Otherwise just pass the other deps through to the constrained pip install
*)
passthrough_args+=("$1")
;;
esac
shift 1
done
# If *only* had special args then then isn't any need to run pip.
if [ -n "$passthrough_args" ] ; then
pip_install "${passthrough_args[@]}"
fi

37
tox.ini
View File

@@ -1,37 +0,0 @@
[tox]
minversion = 2.0
envlist = py35,py27,pep8
[testenv]
setenv =
VIRTUAL_ENV={envdir}
BRANCH_NAME=master
CLIENT_NAME=cliff
passenv =
ZUUL_CACHE_DIR
distribute = False
install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
commands =
python setup.py test --coverage --coverage-package-name=cliff --slowest --testr-args='{posargs}'
coverage report --show-missing
deps = -r{toxinidir}/test-requirements.txt
[testenv:pep8]
deps = flake8
commands = flake8 cliff doc/source/conf.py setup.py
[testenv:venv]
commands = {posargs}
[testenv:neutronclient-tip]
basepython = python2.7
deps = os:openstack/python-neutronclient:python-neutronclient
commands = {toxinidir}/integration-tests/neutronclient-tip.sh {envdir}
[testenv:openstackclient-tip]
basepython = python2.7
deps = os:openstack/python-openstackclient:python-openstackclient
commands = {toxinidir}/integration-tests/openstackclient-tip.sh {envdir}
[testenv:docs]
commands = python setup.py build_sphinx