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:
43
.gitignore
vendored
43
.gitignore
vendored
@@ -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.*
|
@@ -1,4 +0,0 @@
|
||||
[gerrit]
|
||||
host=review.openstack.org
|
||||
port=29418
|
||||
project=openstack/cliff.git
|
@@ -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
|
@@ -1,8 +0,0 @@
|
||||
language: python
|
||||
python:
|
||||
- 2.7
|
||||
- 3.2
|
||||
- 3.3
|
||||
- pypy
|
||||
install: pip install -r test-requirements.txt
|
||||
script: nosetests -d
|
@@ -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
202
LICENSE
@@ -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.
|
@@ -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
|
||||
|
21
Makefile
21
Makefile
@@ -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
14
README
Normal 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.
|
23
README.rst
23
README.rst
@@ -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
|
@@ -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 ()
|
423
cliff/app.py
423
cliff/app.py
@@ -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
|
@@ -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
|
186
cliff/command.py
186
cliff/command.py
@@ -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
|
@@ -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)
|
@@ -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
|
120
cliff/display.py
120
cliff/display.py
@@ -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)
|
@@ -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
|
||||
"""
|
@@ -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
|
@@ -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)
|
@@ -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
|
@@ -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)
|
@@ -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
|
@@ -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)
|
103
cliff/help.py
103
cliff/help.py
@@ -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
|
@@ -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
|
||||
"""
|
@@ -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()
|
@@ -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
|
@@ -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()))
|
@@ -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)
|
@@ -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))
|
@@ -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)
|
@@ -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(),
|
||||
)
|
@@ -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())
|
@@ -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)
|
@@ -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)
|
@@ -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])
|
@@ -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)
|
@@ -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)
|
@@ -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)
|
@@ -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)))
|
@@ -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)
|
@@ -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)
|
@@ -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)
|
@@ -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)
|
@@ -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,
|
||||
)
|
@@ -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,
|
||||
)
|
@@ -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)
|
@@ -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)
|
@@ -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)
|
155
cliff/utils.py
155
cliff/utils.py
@@ -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
|
@@ -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
|
||||
$
|
@@ -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],
|
||||
)
|
@@ -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')
|
@@ -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('.'))
|
||||
)
|
@@ -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:]))
|
@@ -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)
|
@@ -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')
|
@@ -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,
|
||||
)
|
153
doc/Makefile
153
doc/Makefile
@@ -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."
|
@@ -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'
|
@@ -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:
|
@@ -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`
|
@@ -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
|
@@ -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:
|
@@ -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
|
||||
|
@@ -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.
|
@@ -1 +0,0 @@
|
||||
.. include:: ../../../ChangeLog
|
@@ -1,15 +0,0 @@
|
||||
=============
|
||||
Using cliff
|
||||
=============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
introduction
|
||||
demoapp
|
||||
list_commands
|
||||
show_commands
|
||||
complete
|
||||
interactive_mode
|
||||
sphinxext
|
||||
history
|
@@ -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)
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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
|
51
setup.cfg
51
setup.cfg
@@ -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
|
29
setup.py
29
setup.py
@@ -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)
|
@@ -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
|
@@ -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
37
tox.ini
@@ -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
|
Reference in New Issue
Block a user