cliff-3.1.0/0000775000175000017500000000000013637354630012632 5ustar zuulzuul00000000000000cliff-3.1.0/doc/0000775000175000017500000000000013637354630013377 5ustar zuulzuul00000000000000cliff-3.1.0/doc/source/0000775000175000017500000000000013637354630014677 5ustar zuulzuul00000000000000cliff-3.1.0/doc/source/index.rst0000664000175000017500000000075713637354530016550 0ustar zuulzuul00000000000000======================================================= 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` cliff-3.1.0/doc/source/contributors/0000775000175000017500000000000013637354630017434 5ustar zuulzuul00000000000000cliff-3.1.0/doc/source/contributors/index.rst0000664000175000017500000000515413637354530021301 0ustar zuulzuul00000000000000================== 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 https://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: cliff-3.1.0/doc/source/reference/0000775000175000017500000000000013637354630016635 5ustar zuulzuul00000000000000cliff-3.1.0/doc/source/reference/index.rst0000664000175000017500000000163513637354530020502 0ustar zuulzuul00000000000000======================= 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: cliff-3.1.0/doc/source/user/0000775000175000017500000000000013637354630015655 5ustar zuulzuul00000000000000cliff-3.1.0/doc/source/user/index.rst0000664000175000017500000000044413637354530017517 0ustar zuulzuul00000000000000============= Using cliff ============= .. toctree:: :maxdepth: 2 introduction demoapp list_commands show_commands complete interactive_mode sphinxext .. history contains a lot of sections, toctree with maxdepth 1 is used. .. toctree:: :maxdepth: 1 history cliff-3.1.0/doc/source/user/list_commands.rst0000664000175000017500000000706013637354530021245 0ustar zuulzuul00000000000000=============== 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 ` 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 cliff-3.1.0/doc/source/user/demoapp.rst0000664000175000017500000002152313637354530020036 0ustar zuulzuul00000000000000======================== 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 For more information, refer to the autogenerated documentation :ref:`below `. 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 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. .. _demoapp-sphinx: Autogenerated Documentation =========================== The following documentation is generated using the following directive, which is provided by :doc:`the cliff Sphinx extension `. .. code-block:: rest .. autoprogram-cliff:: cliffdemo.main.DemoApp :application: cliffdemo .. autoprogram-cliff:: cliff.demo :application: cliffdemo Output ------ Global Options ~~~~~~~~~~~~~~ .. autoprogram-cliff:: cliffdemo.main.DemoApp :application: cliffdemo Command Options ~~~~~~~~~~~~~~~ .. autoprogram-cliff:: cliff.demo :application: cliffdemo cliff-3.1.0/doc/source/user/complete.rst0000664000175000017500000000230413637354530020215 0ustar zuulzuul00000000000000==================== 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 cliff-3.1.0/doc/source/user/sphinxext.rst0000664000175000017500000001571213637354530020446 0ustar zuulzuul00000000000000==================== Sphinx Integration ==================== Usage ===== cliff supports integration with Sphinx by way of a `Sphinx directives`__. Preparation ----------- Before using the :rst:dir:`autoprogram-cliff` directive you must add `'cliff.sphinxext'` extension module to a list of `extensions` in `conf.py`: .. code-block:: python extensions = ['cliff.sphinxext'] Directive --------- .. rst:directive:: .. autoprogram-cliff:: or Automatically document an instance of :py:class:`cliff.command.Command` or :py:class:`cliff.app.App` including a description, usage summary, and overview of all options. .. important:: There are two modes in this directive: **command** mode and **app** mode. The directive takes one required argument and the mode is determined based on the argument specified. The **command** mode documents various information of a specified instance of :py:class:`cliff.command.Command`. The **command** mode takes the namespace that the command(s) can be found in as the argument. This is generally defined in the `entry_points` section of either `setup.cfg` or `setup.py`. You can specify which command(s) should be displayed using `:command:` option. .. code-block:: rst .. autoprogram-cliff:: openstack.compute.v2 :command: server add fixed ip The **app** mode documents various information of a specified instance of :py:class:`cliff.app.App`. The **app** mode takes the python path of the corresponding class as the argument. In the **app** mode, `:application:` option is usually specified so that the command name is shown in the rendered output. .. code-block:: rst .. autoprogram-cliff:: cliffdemo.main.DemoApp :application: cliffdemo 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. This option is effective only in the **command** mode. `:arguments` The arguments to be passed when the cliff application is instantiated. Some cliff applications requires arguments when instantiated. This option can be used to specify such arguments. This option is effective only in the **app** mode. `: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. `autoprogram_cliff_app_dist_name` The name of the python distribution (the name used with pip, as opposed to the package name used for importing) providing the commands/applications being documented. Generated documentation for plugin components includes a message indicating the name of the plugin. Setting this option tells cliff the name of the distribution providing components natively so their documentation does not include this message. .. 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: Examples ======== Simple Example (`demoapp`) -------------------------- `cliff` provides a sample application, :doc:`demoapp`, to demonstrate some of the features of `cliff`. This application :ref:`is documented ` using the `cliff.sphinxext` Sphinx extension. Advanced Example (`python-openstackclient`) ------------------------------------------- It is also possible to document larger applications, such as `python-openstackclient`__. Take a sample `setup.cfg` file, which is a minimal version of the `setup.cfg` provided by 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 __ https://docs.openstack.org/python-openstackclient/ cliff-3.1.0/doc/source/user/introduction.rst0000664000175000017500000000540713637354530021135 0ustar zuulzuul00000000000000============== 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 cliff-3.1.0/doc/source/user/history.rst0000664000175000017500000000004013637354530020101 0ustar zuulzuul00000000000000.. include:: ../../../ChangeLog cliff-3.1.0/doc/source/user/show_commands.rst0000664000175000017500000000670213637354530021254 0ustar zuulzuul00000000000000=============== 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 ` 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 cliff-3.1.0/doc/source/user/interactive_mode.rst0000664000175000017500000000516713637354530021740 0ustar zuulzuul00000000000000================== 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 ): =================================== 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 ): ========================================= 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) cliff-3.1.0/doc/source/install/0000775000175000017500000000000013637354630016345 5ustar zuulzuul00000000000000cliff-3.1.0/doc/source/install/index.rst0000664000175000017500000000150513637354530020206 0ustar zuulzuul00000000000000============== Installation ============== Python Versions =============== cliff is being developed under Python 3. 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 OpenDev: https://opendev.org/openstack/cliff Reporting Bugs ============== Please report bugs through the github project: https://bugs.launchpad.net/python-cliff cliff-3.1.0/doc/source/conf.py0000664000175000017500000002031613637354530016177 0ustar zuulzuul00000000000000# -*- 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 os.path import sys # make openstackdocstheme an optional dependency. cliff is a low level lib # that is used outside of OpenStack. Not having something OpenStack specific # as build requirement is a good thing. try: import openstackdocstheme except ImportError: has_openstackdocstheme = False else: has_openstackdocstheme = True # 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. # NOTE(dhellmann): Add the demoapp directory to the import path so the # directive for documenting the app can find the modules. sys.path.insert(0, os.path.abspath('../../demoapp')) # -- 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', 'cliff.sphinxext', ] if has_openstackdocstheme: extensions.append('openstackdocstheme') # openstackdocstheme options repository_name = 'openstack/cliff' bug_project = 'python-cliff' bug_tag = '' # 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 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' if has_openstackdocstheme: 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 = [] if has_openstackdocstheme: html_theme_path = [openstackdocstheme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v 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 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' cliff-3.1.0/doc/requirements.txt0000664000175000017500000000044713637354530016667 0ustar zuulzuul00000000000000# 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. sphinx!=1.6.6,!=1.6.7,!=2.1.0,>=1.6.2 # BSD openstackdocstheme>=1.18.1 # Apache-2.0 cliff-3.1.0/README.rst0000664000175000017500000000165613637354530014330 0ustar zuulzuul00000000000000======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/cliff.svg :target: https://governance.openstack.org/tc/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: https://docs.openstack.org/cliff/latest/ * Source: https://opendev.org/openstack/cliff * Bugs: https://bugs.launchpad.net/python-cliff * Contributors: https://github.com/openstack/cliff/graphs/contributors cliff-3.1.0/tox.ini0000664000175000017500000000303513637354530014145 0ustar zuulzuul00000000000000[tox] minversion = 3.1.0 envlist = py37,pep8 ignore_basepython_conflict = True [testenv] basepython = python3 setenv = VIRTUAL_ENV={envdir} OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 distribute = False commands = python setup.py test --coverage --coverage-package-name=cliff --slowest --testr-args='{posargs}' coverage report --show-missing deps = -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/upper-constraints.txt} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt [testenv:pep8] deps = -r{toxinidir}/test-requirements.txt flake8 commands = flake8 cliff doc/source/conf.py setup.py # Run security linter bandit -c bandit.yaml -r cliff -x tests -n5 [testenv:venv] # TODO(modred) remove doc/requirements.txt once the openstack-build-sphinx-docs # job is updated. deps = -r{toxinidir}/test-requirements.txt -r{toxinidir}/doc/requirements.txt commands = {posargs} [testenv:neutronclient-tip] deps = os:openstack/python-neutronclient:python-neutronclient commands = {toxinidir}/integration-tests/neutronclient-tip.sh {envdir} [testenv:openstackclient-tip] deps = os:openstack/python-openstackclient:python-openstackclient commands = {toxinidir}/integration-tests/openstackclient-tip.sh {envdir} [testenv:docs] deps = -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W -b html doc/source doc/build/html [testenv:lower-constraints] deps = -c{toxinidir}/lower-constraints.txt -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt cliff-3.1.0/.stestr.conf0000664000175000017500000000004713637354530015103 0ustar zuulzuul00000000000000[DEFAULT] test_path=./cliff top_dir=./ cliff-3.1.0/bandit.yaml0000664000175000017500000000002013637354530014746 0ustar zuulzuul00000000000000skips: - B110 cliff-3.1.0/demoapp/0000775000175000017500000000000013637354630014257 5ustar zuulzuul00000000000000cliff-3.1.0/demoapp/README.rst0000664000175000017500000000170013637354530015743 0ustar zuulzuul00000000000000================= 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 $ cliff-3.1.0/demoapp/setup.py0000664000175000017500000000363013637354530015772 0ustar zuulzuul00000000000000#!/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, ) cliff-3.1.0/demoapp/cliffdemo/0000775000175000017500000000000013637354630016207 5ustar zuulzuul00000000000000cliff-3.1.0/demoapp/cliffdemo/__main__.py0000664000175000017500000000015113637354530020275 0ustar zuulzuul00000000000000import sys from cliffdemo.main import main if __name__ == '__main__': sys.exit(main(sys.argv[1:])) cliff-3.1.0/demoapp/cliffdemo/encoding.py0000664000175000017500000000071113637354530020345 0ustar zuulzuul00000000000000# -*- 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], ) cliff-3.1.0/demoapp/cliffdemo/list.py0000664000175000017500000000061213637354530017532 0ustar zuulzuul00000000000000import 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('.')) ) cliff-3.1.0/demoapp/cliffdemo/show.py0000664000175000017500000000145413637354530017544 0ustar zuulzuul00000000000000import 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) cliff-3.1.0/demoapp/cliffdemo/hook.py0000664000175000017500000000274613637354530017531 0ustar zuulzuul00000000000000# 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') cliff-3.1.0/demoapp/cliffdemo/__init__.py0000664000175000017500000000000013637354530020305 0ustar zuulzuul00000000000000cliff-3.1.0/demoapp/cliffdemo/main.py0000664000175000017500000000153213637354530017505 0ustar zuulzuul00000000000000import 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:])) cliff-3.1.0/demoapp/cliffdemo/simple.py0000664000175000017500000000104713637354530020053 0ustar zuulzuul00000000000000import 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') cliff-3.1.0/ChangeLog0000664000175000017500000005336713637354630014422 0ustar zuulzuul00000000000000CHANGES ======= 3.1.0 ----- * Re-add support for python 3.5 * Fix nested argument groups with ignore conflict handler * adding missing releasenote for the drop of py27 support 3.0.0 ----- * [ussuri][goal] Drop python 2.7 support and testing 2.18.0 ------ * Add autoprogram\_cliff\_app\_dist\_name config opt * Switch to Ussuri jobs * Add contributors link to readme 2.16.0 ------ * Pin cmd2 back to <0.9 on all versions * Modify the help message of \`-c\`/\`--column\` parameter * Add Python 3 Train unit tests * Stop wildcard importing argparse 2.15.0 ------ * Add an errexit attribute to InteractiveApp to exit on command errors * Dropping the py35 testing * Updates for OpenDev transition * OpenDev Migration Patch * add python 3.7 unit test job * Missing carriage return in some cases, using -f json 2.14.1 ------ * Use template for lower-constraints * Change openstack-dev to openstack-discuss 2.14.0 ------ * Don't try to run issubclass on non-classes * Removed unused err variable * Remove dead files * add lib-forward-testing-python3 test job * add python 3.6 unit test job * switch documentation job to new PTI * import zuul job settings from project-config 2.13.0 ------ * Assure executable name is kept when app is called as module 2.12.1 ------ * Build universal wheels * fix tox python3 overrides * support cmd2 0.9.1 in interactive mode 2.12.0 ------ * update cmd2 dependency to handle py3 only versions * Remove travis.yml * exclude cmd2 0.8.3 and update to 0.8.4 * add lower-constraints job * fix typos in documentation 2.17.0 ------ * Allow finding command by partial name * Updated from global requirements * Remove the warning of getargspec removal * Align parsed() call with cmd2 versions >= 0.7.3 * Fix cmd2 doc URL * add argparse conflict handler "ignore" * sphinxext: Warn if namespace or command pattern invalid * Zuul: Remove project name * Updated from global requirements 2.11.0 ------ * remove -s alias for --sort-columns 2.10.0 ------ * Remove empty files * Add ability to sort data by columns for list commands * Updated from global requirements * Remove tox\_install.sh and just pass -c in tox * Replace legacy tips jobs with shiny new versions * Move doc requirements to doc/requirements.txt * do not require installing demo app to build docs * add support for legacy command name translation * Use in-tree cliffdemo app for docs build * Updated from global requirements * add bandit to pep8 job * sphinxext: Support cliff application * Fix PEP8 in gate * doc: Cleanup of demoapp doc * Generate demoapp CLI refernece * Fix codec error when format=csv 2.9.1 ----- * handle more varied top\_level.txt files in distributions 2.9.0 ----- * show the distribution providing the command in help output * Update .gitignore * Docs update for more-hooks * Updates for stestr * Allow command hooks to make changes * Updated from global requirements * add actual column names to error msg Closes-Bug: 1712876 * Alias exit to cmd2's quit command to exit interactive shell * Updated from global requirements * Update doc on Sphinx integration process * Fix regexp for detecting long options * sphinxext: Correct issues with usage formatting * Move comments up in [extras] section of setup.cfg * Updated from global requirements * Make openstackdocstheme an optional doc dependency * Updated from global requirements * doc: minor cleanup * Update and replace http with https for doc links * doc: Remove blank lines between term and definition * trivial: Fix comments in sphinxext module * Use assertIsNone(...) instead of assertIs(None,...) * Updated from global requirements 2.8.0 ----- * add tests for display command classes and hooks * Run hooks for DisplayCommandBase * add --fit-width option to table formatter * sphinxext: Add 'application' option to the autoprogram directive * use openstackdocstheme html context * switch from oslosphinx to openstackdocstheme * Fix erroneous line in command hook test * make smart help formatter test deterministic * remove references to distribute in the docs * add before and after hooks * add hook for get\_epilog * add hook for manipulating the argument parser * Updated from global requirements * pass the command name from HelpCommand * Adjust completenames tests for cmd2 0.7.3+ * rearrange existing content to follow new standard * sphinext: Use metavar where possible * sphinxext: Use 'argparse.description', 'argparse.epilog' * sphinxext: Allow configuration of ignorable options * sphinxext: Generate better usage examples * add cmd\_name argument to CompleteCommand * Ensure python standard argparse module is loaded * Updated from global requirements 2.7.0 ----- * covert test suite to use testrepository 2.6.0 ----- * Updated from global requirements * Add smart help formatter for command parser * Add support for epilogs * Add 'autoprogram-cliff' Sphinx directive * .gitignore: Ignore eggs 2.5.0 ----- * Use Sphinx 1.5 warning-is-error * Update cmd2 fix to still work with 0.6.7 * Remove support for py34 * Fix broken test with cmd2 0.7.0 * Updated from global requirements * Updated from global requirements * Updated from global requirements * Add newline if the output formatter is json 2.4.0 ----- * Add Constraints support * Remove tox environments that no longer work * Fix command order * Show team and repo badges on README * Add print\_empty parameter 2.3.0 ----- * ignore Command docstring when looking for descriptions * let the Command get its one-liner description from a class attribute * flake8 fix * Replace dashes and colons when using bash formatter * Show entire command in error message * Updated from global requirements * Updated from global requirements * Fix spelling mistake * Add Python 3.5 classifier and venv * Updated from global requirements * Changed the home-page link * Add Apache 2.0 license to source file * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Clean imports in code * [doc]Fix URL for 'setuptools entry points' * Fix a typo in comment 2.2.0 ----- * Avoid ASCII encoding errors when output is redirected * Fix cliff URLs in doc and demoapp * Remove announce.rst * Fix Command class doc typo * Updated from global requirements * Fixed broken link * add formattable columns concept * Add tests, cover more cases * Updated from global requirements * pep8: fix F405 error * command: make run() return take\_action() value 2.1.0 ----- * Updated from global requirements * Update --max-width help * Add more test coverage for shell formatter * Add more test coverage for CSV formatter * Support multiple sub commands in completion * Factorize more test data * Factorize some test data * Factorize common test code * Factorize argparse importing * Updated from global requirements * Updated from global requirements * Add CLIFF\_MAX\_TERM\_WIDTH envvar to complement --max-width * Fix prettytable.PrettyTable().max\_width wrong usage * Fix AttributeError when entry point load failed * Distinguish no existed columns in ShowOne * Refactor HelpCommand * Updated from global requirements * Remove httplib2 from test-requirements.txt * Sync help message for --help 2.0.0 ----- * handle empty list in table formatter 1.17.0 ------ * Drop Python 2.6 support * Revert "app,command: disallow abbrev by default" * Fixes terminal\_width on Windows 1.16.0 ------ * Updated from global requirements * remove openstack-common.conf * Add doc for deferred\_help parameter * Fix pep8 failure * app,command: disallow abbrev by default * app: work-around abbrev * remove unnecessary dependency on argparse * Make verbose and quiet mutually exclusive * setup: fix Python versions classifiers * Don't import unused logging * Don't use non-existent method of Mock * Replace dashes with underscores in bash completion * Updated from global requirements * Resize columns to fit screen width * fix fuzzy search for same-distance case * Correct path to docs * only use unicodecsv for python 2.x * Fix test class docstring for py 3.5 1.15.0 ------ * Replace '\r' with ' ' for prettytable * Implement a json formatter in cliff * Implement a yaml formatter in cliff * Updated from global requirements * Improve help messages 1.14.0 ------ * Add csv formatter test * Fix encoding issue with the default python csv output * Remove py26 as a default test from tox.ini * Set demo app up with deferred help * Add command fuzzy matching * Updated from global requirements * Remove requirements.txt from tox.ini * Updated from global requirements * Updated from global requirements * Allow subcommands to accept --help when using 'deferred\_help' * Updated from global requirements * Fix logging config in demo app * Use base command for help test * Updated from global requirements * Include the automatically-generated changelog * Updated from global requirements 1.13.0 ------ * Fix object has no attribute debug error * Add some docs for list value formatter * Add value format for list command * Updated from global requirements * Remove run\_cross\_tests.sh * fix author contact details * Print help on help command 1.12.0 ------ * Do not check requirements when loading plugins 1.11.0 ------ * Catch and ignore error when locale can not be set * Uncap library requirements for liberty * Add documentation for the value formatter * Sort the fuzzy matches * Defer interactive import * Updated from global requirements * Update links to setuptools doc 1.10.1 ------ * Pass user command text to the Command object * Document print\_help\_if\_requested method 1.10.0 ------ * Allow to call initialize\_app when running --help * Hide prompt in batch/pipe mode * Correct completion in interactive mode * Change the argument passed to \_\_init\_\_ for help * Fix pep8 tests for lambda * Updated from global requirements * Fix git repo urls in tox.ini * Add deprecated attribute to commands * Workflow documentation is now in infra-manual 1.9.0 ----- * print the real error cmd argument * Updated from global requirements 1.8.0 ----- * Update link to docs in README * Bring doc build up to standard * Add pbr to installation requirements * Add more detail to the README * Updated from global requirements * Add docs environment to tox.ini * mock.assert\_called\_once() is not a valid method * Work toward Python 3.4 support and testing * warn against sorting requirements 1.7.0 ----- * Add release notes for 1.7.0 * Fix stable integration tests * Updated from global requirements * Clean up default tox environment list * Do not allow wheels for stable tests * Set the main logger name to match the application * CSV formatter should use system-dependent line ending * Make show option compatible with Python 2.6 * Use six.add\_metaclass instead of \_\_metaclass\_\_ * fixed typos found by RETF rules * The --variable option to shell format is redundant * Expose load\_commands publicly * Fix wrong method name assert\_called\_once * Updated from global requirements * Fix pep8 failures on rule E265 1.6.1 ----- * Remove PrettyTable from documentation requirements * Fix a bug in ShellFormatter's escaping of double quotes in strings * Import run\_cross\_tests.sh from oslo-incubator * add doc requirements to venv 1.6.0 ----- * Add max-width support for table formatter * Add value only output formattter * Update readme with links to bug tracker and source * Move pep8 dependency into pep8 tox test * Fix doc build with Python 2.6.x * Fix interactive mode with command line args * Update .gitreview after repo rename * Escape double quotes in shell formatter * Add unit test for shell formatter * Rename private attribute to avoid conflict * Sync with global requirements * Add integration tests with known consumers * update history for previous change * Make the formatters a private part of the command 1.5.2 ----- * move to pbr for packaging 1.5.1 ----- * add venv environ to tox config 1.5.0 ----- * Update history for next release * Move to stackforge * update history for stevedore change * Use stevedore to load formatter plugins * use entry points for completion plugins * Clean up recursive data handling * Always install complete command * attribution for bash completion work in history * code style fixes * code style fixes * various python code optimizations; shuffle I/O to shell classes * add bash complete * Enable debug in help mode * Pass the right args when pulling help from commands * prepare for 1.4.5 release * add pypy test env configuration * Update pyparsing dependency to 2.0.1 1.4.4 ----- * update for release 1.4.4 * Re-raise Exception on debug mode * Add test to check if return code is 2 on unknown command * Return code 1 is already use, use code 2 instead * Reraise error on debug * Display better error message on unknown command, and return code 1 * update announce file 1.4.3 ----- * prepare for 1.4.3 release * force python2.6 for that test env * Provide a default output encoding 1.4.2 ----- * prepare for release 1.4.2 1.4.1 ----- * prepare for release 1.4.1 * Tighten requirements on cmd2 * remove use of distribute in demo app * Fix default encoding issue with python 2.6 * move tests into cliff package * add tests for dict2columns * Add dict2columns() * turn off distribute in tox 1.4 --- * prep for release 1.4 * fix flake8 issues with setup.py * remove the other traces of distribute * Remove explicit depend on distribute * update history for recent contribution * Expose instantiated interpreter instance and assign it to the 'interpreter' variable on the App instance * Update announcement for release 1.3.3 1.3.3 ----- * Prepare for release 1.3.3 * declare support for python 3.3 * cmd2 0.6.5.1 was released, and is compatible * Restore compatibility with Prettytable < 0.7.2 1.3.2 ----- * Prepare 1.3.2 release * Bump prettytable version accepted * add python 3.3 to tox * add style checks to tests * Add tests for underscore handling * use flake8 for style checks * update history.rst with convert\_underscores change * make converting underscores optional in CommandManager * fix version in docs 1.3.1 ----- * prepare for 1.3.1 release * Fix PyParsing dependency * Fix typo * update history file for previous merge * Make list of application commands lexicographically ordered for help command in interactive mode 1.3 --- * Prepare for 1.3 release * clean up history file * Document dependency on distribute * fix rst formatting in docstring * Update history file * Add tests for new functionality * Allow user to pass argparse\_kwargs argument to the build\_option\_parser method. Those arguments gets passed to the ArgumentParser constructor 1.2.1 ----- * Set up for 1.2.1 release * Remove unused logging import * Fix problem with missing izip import in lister.py * Update announcement file for new release 1.2 --- * Set up release 1.2 * Add python2.6 support * remove debug print * remove tablib from test requirements * Fix logging default behavior * Fix interactive help command 1.1.2 ----- * bumping version number for release * remove the entry point data for the moved formatters 1.1.1 ----- * bump the version number to release a clean build 1.1 --- * Update version and status values * Remove tablib formatters from core * fix version # in announcement 1.0 --- * Doc updates for API changes. Clean up docstrings. Bump version to 1.0 * merge API refactoring work * yet more pep8 work * fix help and tests for API change * Move take\_action() to Command * more pep8 work * Refactor DisplayBase.run() to make it easier to override separate parts in subclasses. Rename get\_data() to take\_action() so subclasses that do something other than query for values have a clear place to override * pep8 cleanup * add attribution to history for the previous merge * Adding new line to tablib formatters * fix tags declaration * document updates for 0.7 * disable py26 tests since I do not have an environment for running them 0.7 --- * bump version * fix interactive command processor to handle multi-part commands, including some that use the same first word as existing commands * declare a couple of commands that use builtin command names but use multiple words * update changelog * set the interactive mode flag before initializing the app so subclasses can check it; handle initialization errors more cleanly * add travis-ci status image to developer docs * add travis-ci status image to README * add a requirements file for travis-ci * bogus commit to trigger ci build * add travis-ci.org configuration file * add version num to history file 0.6 --- * bump version number * pass more details to initialize\_app so subclasses can decide what sort of initialization to do * enable to use in Python2.6 0.5.1 ----- * remove hard version requirement to unbreak the OpenStack build 0.5 --- * prepare for 0.5 release * document changes in history file * make the organization of the classes page a little more clear * update formatter documentation * fix yaml, html, and json show formatters * move the column option so it applies to "show" commands, too * add yaml, json, and html formatters * move the columns option out of the table formatter and into the lister base * make help list commands if none match exactly; fixes #8 * require at least PrettyTable 0.6 for Python 3 support, fixes #7 * changes in the prettytable API rolled into the python 3 support update * add a tox stage for pep8 testing * python 3.2 does not have a unicode type so ignore the error if it is missing * move todo list to github issues * update todo list * note about prettytable and python3 * refactor ShowOne and Lister to share a common base class * more todo notes * tests for cliff.help * pass the App to the help action instead of passing just the command manager, since the app has the stout handle we want to use for printing the help * 100% coverage of cliff.command * 100% coverage for commandmanager.py * 100% coverage of cliff.app module * let the interactive app set its own prompt * add tests for App and fix an issue with error handling and clean\_up() in python 3 * use the stderr handle given to the app instead of assuming the default 0.4 --- * version number and release note updates for 0.4 * documentation improvements * simplify packaging file for demo app * ignore files generated by dev environment * first pass at interactive app * note to add more options to csv formatter * add --prefix option for shell formatter; add docs for shell formatter * clean up help text for the other formatters * add shell output formatter for single items * add longer docstring to show how it is printed by help * update todo list * fix typo in blog post 0.3 --- * update blog announcement * bump the version number and update the release notes * add ShowOne base class for commands that need to show properties of an individual object make the table formatter work as a single object formatter update the docs for the new features * handle an empty data set * correct the doctoring * fix version # in doc build script * 0.2 release announcement post 0.2 --- * bump version number * start a release log * update doc instructions for getting help * only show the one-line description in the command list; add a description of "help" * register a custom help action that knows how to print the list of commands available and a help command to generate help for the other commands * provide an internal API for applications to register commands without going through setuptools (used for help handler) * Use argparse for global arguments * fix doc build instructions * add some developer instructions and links ot the source repo and bug tracker * add announcement blog post source * advice from the distutils list was to stick with distribute for now * add Makefile with some common release operations * add example output to the list formatters * add a requirements file for doc build on readthedocs.org * add some real documentation * Add get\_data() to the Lister base class * remove example that I was using as a syntax reminder * Add a link to the docs * while looking for documentation on entry points I realized distutils2 doesn't seem to support them in the same way * fill in a real description of the project * start sphinx documentation * Added a bit more to the README * flesh out instructions for using the demo app * add a few more ideas * Added a README for the demo app * Added download url to both setup.py files and updated the demo setup.py with the new url 0.1 --- * Added missing distribute setup file * move repo link to the dreamhost project * more to-do items * add demoapp to release package and clean up files being distributed from the test directory * notes about work still to be done * require PrettyTable package for the table formatter * improve error handling when loading formatter plugins * add a csv formatter for list apps * start creating a subclass of command for producing a list of output in different formats, using prettytable as an example formatter * remove unused import * better error handling of post-action hook in app * Pass the I/O streams into the app * add some error handling to App * make the log messages slightly easier to parse * tweak App api to make it easier to override and perform global actions before and after a command runs * use logging for controlling console output verbosity * clean up argv handling * install nose for tox tests * if no arguments are provided at all show the help message * replace default --help processor with one that includes the list of subcommands available * add debug option to nose * clean up dead code * include version info when configuring opt parse * Sample program with command plugins * first pass at an app class that can invoke commands * save commands using the name representation to be used in help output; don't modify the input arg list when searching for the command; return the name of the command found so the app can stuff it into the help text of the command * start building command manager * change to apache license * add tox config file for tests * add distribute\_setup.py so install works * add setup.py and package directory * add a basic description to readme * convert readme to rst * initial commit cliff-3.1.0/PKG-INFO0000664000175000017500000000372713637354630013740 0ustar zuulzuul00000000000000Metadata-Version: 1.2 Name: cliff Version: 3.1.0 Summary: Command Line Interface Formulation Framework Home-page: https://docs.openstack.org/cliff/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: UNKNOWN Description: ======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/cliff.svg :target: https://governance.openstack.org/tc/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: https://docs.openstack.org/cliff/latest/ * Source: https://opendev.org/openstack/cliff * Bugs: https://bugs.launchpad.net/python-cliff * Contributors: https://github.com/openstack/cliff/graphs/contributors Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Intended Audience :: Developers Classifier: Environment :: Console Requires-Python: >=3.5 cliff-3.1.0/AUTHORS0000664000175000017500000000731713637354630013712 0ustar zuulzuul00000000000000Akihiro Motoki Alessandro Pilotti Andrea Frittoli Andreas Jaeger Andreas Jaeger Andrew Spiers Andrey Volkov Atsushi SAKAI Brano Zarnovican Cao Xuan Hoang Cedric Brandily ChangBo Guo(gcb) Christian Berendt Christophe CHAUVET Clint Byrum Corey Bryant Dan Kirkwood Dean Troyer Derek Higgins Dirk Mueller Dirk Mueller Doug Hellmann Doug Hellmann Doug Hellmann Duncan McGreggor Eric Fried Eyal Posener Feodor Tersin Flavio Percoco Ghanshyam Mann Hervé Beraud Hongbin Lu Ilya Shakhat James Downs James E. Blair Jamie Lennox Jaspreet Singh Rawel Jeremy Stanley Joe Server John Dennis Jonathan LaCour Juan Antonio Osorio Robles Julien Danjou KATO Tomoyuki Ken'ichi Ohmichi Kien Nguyen Maciej Kwiek Mark Goddard Mark McClain Masayuki Igawa Masayuki Igawa Matt Joyce Matthew Edmonds Matthew Thode Michael Davies Monty Taylor Neil Borle Nguyen Hung Phuong Nicola Soranzo Pierre-André MOREY Qiu Yu Rajath Agasthya Ricardo Kirkner Rui Chen Ruslan Aliev Ryan Petrello Ryan Selden Sascha Peilicke Sean Perry Sorin Sbarnea Stephen Finucane Steve Baker Steve Martinelli Terry Howe TerryHowe Thomas Bechtold Thomas Herve Tomaz Muraus Tony Breeds Tony Xu Vincent Legoll Vitalii Kulanov Yalei Wang Yossi Ovadia Yushiro FURUKAWA ZhongShengping caoyue dineshbhor gcmalloc gengchc2 heavenshell howardlee kafka kangyufei lingyongxu liyingjun markmcclain melissaml qingszhao qneill shizhihui xuanyandong yanpuqing cliff-3.1.0/CONTRIBUTING.rst0000664000175000017500000000052113637354530015270 0ustar zuulzuul00000000000000Changes to cliff should be submitted for review via the Gerrit tool, following the workflow documented at: https://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 cliff-3.1.0/cliff/0000775000175000017500000000000013637354630013715 5ustar zuulzuul00000000000000cliff-3.1.0/cliff/sphinxext.py0000664000175000017500000003133013637354530016320 0ustar zuulzuul00000000000000# 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 importlib import inspect import re import sys from docutils import nodes from docutils.parsers import rst from docutils.parsers.rst import directives from docutils import statemachine from cliff import app 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 ' # becomes ['--format '] and not ['--format', '']. # Yes, they really do use regexes to break apart and rewrap their help # string. Don't ask me why. part_regexp = re.compile(r""" \(.*?\)+ | \[.*?\]+ | (?:(?:-\w|--\w+(?:-\w+)*)(?:\s+?)?) | \S+ """, re.VERBOSE) opt_usage = fmt._format_actions_usage(optionals, groups) pos_usage = fmt._format_actions_usage(positionals, groups) opt_parts = part_regexp.findall(opt_usage) pos_parts = part_regexp.findall(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='') >>> 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] .. 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, 'arguments': directives.unchanged, 'ignored': directives.unchanged, 'application': directives.unchanged, } def _get_ignored_opts(self): 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()] return list(set(global_ignored + local_ignored)) def _drop_ignored_options(self, parser, ignored_opts): 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 def _load_app(self): mod_str, _sep, class_str = self.arguments[0].rpartition('.') if not mod_str: return try: importlib.import_module(mod_str) except ImportError: return try: cliff_app_class = getattr(sys.modules[mod_str], class_str) except AttributeError: return if not inspect.isclass(cliff_app_class): return if not issubclass(cliff_app_class, app.App): return app_arguments = self.options.get('arguments', '').split() return cliff_app_class(*app_arguments) 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 _load_commands(self): # TODO(sfinucan): We should probably add this wildcarding functionality # to the CommandManager itself to allow things like "show me the # commands like 'foo *'" command_pattern = self.options.get('command') 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() if not commands: msg = 'No commands found in the "{}" namespace' if command_pattern: msg += ' using the "{}" command name/pattern' msg += ('. Are you sure this is correct and the application being ' 'documented is installed?') raise self.warning(msg.format(self.arguments[0], command_pattern)) return dict((name, self._load_command(manager, name)) for name in commands) def _generate_app_node(self, app, application_name): ignored_opts = self._get_ignored_opts() parser = app.parser self._drop_ignored_options(parser, ignored_opts) parser.prog = application_name source_name = '<{}>'.format(app.__class__.__name__) result = statemachine.ViewList() for line in _format_parser(parser): result.append(line, source_name) section = nodes.section() self.state.nested_parse(result, 0, section) # return [section.children] return section.children def _generate_nodes_per_command(self, title, command_name, command_class, ignored_opts): """Generate the relevant Sphinx nodes. This doesn't bother using raw docutils nodes as they simply don't offer the power of directives, like Sphinx's 'option' directive. Instead, we generate reStructuredText and parse this in a nested context (to obtain correct header levels). 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) if not getattr(command, 'app_dist_name', None): command.app_dist_name = ( self.env.config.autoprogram_cliff_app_dist_name) parser = command.get_parser(command_name) ignored_opts = ignored_opts or [] self._drop_ignored_options(parser, ignored_opts) 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 _generate_command_nodes(self, commands, application_name): ignored_opts = self._get_ignored_opts() output = [] for command_name in sorted(commands): command_class = commands[command_name] title = command_name if application_name: command_name = ' '.join([application_name, command_name]) output.extend(self._generate_nodes_per_command( title, command_name, command_class, ignored_opts)) return output def run(self): self.env = self.state.document.settings.env application_name = (self.options.get('application') or self.env.config.autoprogram_cliff_application) app = self._load_app() if app: return self._generate_app_node(app, application_name) commands = self._load_commands() return self._generate_command_nodes(commands, application_name) 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) app.add_config_value('autoprogram_cliff_app_dist_name', None, True) cliff-3.1.0/cliff/columns.py0000664000175000017500000000227413637354530015753 0ustar zuulzuul00000000000000# 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 cliff-3.1.0/cliff/utils.py0000664000175000017500000001620313637354530015430 0ustar zuulzuul00000000000000# 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 codecs import ctypes import inspect import os import struct import sys import six # 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} if hasattr(inspect, 'getfullargspec'): getargspec = inspect.getfullargspec else: getargspec = inspect.getargspec 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 if six.PY2: def getwriter(encoding): '''Override codecs.getwriter() to prevent codec errors. The StreamWriter returned by codecs.getwriter has an unfortunate property, it will attempt to encode every object presented to it's write() function. Normally we only want unicode objects to be encoded to a byte stream. If bytes are presented (e.g. str in Python2) we make the assumption those bytes represent an already encoded text stream or they are indeed binary bytes and hence should not be encoded. When the core StreamWriter attempts to encode a str object Python will first promote the str object to a unicode object. The promotion of str to unicode requires the str bytes to be decoded. However the encoding associated with the str object is not known therefore Python applies the default-encoding which is ASCII. In the case where the str object contains utf-8 encoded non-ASCII characters a decoding error is raised. By not attempting to encode a byte stream we avoid this error. It really does not make much sense to try and encode a byte stream. First of all a byte stream should not be encoded if it's not text (e.g. binary data). If the byte stream is encoded text the only way to re-encode it is if we known it's encoding so we can decode it into a canonical form (e.g. unicode). Thus to re-encode it we encode from the canonical form (e.g. unicode) to the new binary encoding. The problem in Python2 is we never know if the bytes in a str object are text or binary data and if it's text which encoding it is, hence we should not try to apply an encoding to a str object. ''' class _StreamWriter(codecs.StreamWriter): def __init__(self, stream, errors='strict'): codecs.StreamWriter.__init__(self, stream, errors) def encode(self, msg, errors='strict'): if isinstance(msg, six.text_type): return self.encoder(msg, errors) return msg, len(msg) _StreamWriter.encoder = codecs.getencoder(encoding) _StreamWriter.encoding = encoding return _StreamWriter else: getwriter = codecs.getwriter cliff-3.1.0/cliff/app.py0000664000175000017500000004033313637354530015051 0ustar zuulzuul00000000000000# 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 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] if NAME == '__main__': NAME = os.path.split(os.path.dirname(sys.argv[0]))[-1] 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 = utils.getwriter(encoding)(sys.stdout) if not (stderr or isinstance(sys.stderr, codecs.StreamWriter)): stderr = utils.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='{0} {1}'.format(App.NAME, 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, ) return self.interpreter.cmdloop() 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 utils.getargspec(cmd_factory.__init__).args: kwargs['cmd_name'] = cmd_name cmd = cmd_factory(self, self.options, **kwargs) 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' here gets caught and does not exit like we want return result 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 cliff-3.1.0/cliff/lister.py0000664000175000017500000000602413637354530015572 0ustar zuulzuul00000000000000# 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 operator 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' @property def need_sort_by_cliff(self): """Whether sort procedure is performed by cliff itself. Should be overridden (return False) when there is a need to implement custom sorting procedure or data is already sorted.""" return True @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 get_parser(self, prog_name): parser = super(Lister, self).get_parser(prog_name) group = self._formatter_group group.add_argument( '--sort-column', action='append', default=[], dest='sort_columns', metavar='SORT_COLUMN', help=("specify the column(s) to sort the data (columns specified " "first have a priority, non-existing columns are ignored), " "can be repeated") ) return parser def produce_output(self, parsed_args, column_names, data): if parsed_args.sort_columns and self.need_sort_by_cliff: indexes = [column_names.index(c) for c in parsed_args.sort_columns if c in column_names] if indexes: data = sorted(data, key=operator.itemgetter(*indexes)) (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 cliff-3.1.0/cliff/formatters/0000775000175000017500000000000013637354630016103 5ustar zuulzuul00000000000000cliff-3.1.0/cliff/formatters/json_format.py0000664000175000017500000000350713637354530021002 0ustar zuulzuul00000000000000# 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) stdout.write('\n') cliff-3.1.0/cliff/formatters/base.py0000664000175000017500000000512313637354530017367 0ustar zuulzuul00000000000000# 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 """ cliff-3.1.0/cliff/formatters/shell.py0000664000175000017500000000457013637354530017571 0ustar zuulzuul00000000000000# 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 cliff-3.1.0/cliff/formatters/table.py0000664000175000017500000001663313637354530017554 0ustar zuulzuul00000000000000# 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='', 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) cliff-3.1.0/cliff/formatters/yaml_format.py0000664000175000017500000000304613637354530020771 0ustar zuulzuul00000000000000# 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) cliff-3.1.0/cliff/formatters/commaseparated.py0000664000175000017500000000463213637354530021446 0ustar zuulzuul00000000000000# 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_kwargs = dict( quoting=self.QUOTE_MODES[parsed_args.quote_mode], lineterminator=os.linesep, escapechar='\\', ) # In Py2 we replace the csv module with unicodecsv because the # Py2 csv module cannot handle unicode. unicodecsv encodes # unicode objects based on the value of it's encoding keyword # with the result unicodecsv emits encoded bytes in a str # object. The utils.getwriter assures no attempt is made to # re-encode the encoded bytes in the str object. if six.PY2: writer_kwargs['encoding'] = (getattr(stdout, 'encoding', None) or 'utf-8') writer = csv.writer(stdout, **writer_kwargs) 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 cliff-3.1.0/cliff/formatters/__init__.py0000664000175000017500000000000013637354530020201 0ustar zuulzuul00000000000000cliff-3.1.0/cliff/formatters/value.py0000664000175000017500000000265513637354530017600 0ustar zuulzuul00000000000000# 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 cliff-3.1.0/cliff/command.py0000664000175000017500000001722013637354530015706 0ustar zuulzuul00000000000000# 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 pkg_resources import six from stevedore import extension from cliff import _argparse _dists_by_mods = None def _get_distributions_by_modules(): """Return dict mapping module name to distribution names. The python package name (the name used for importing) and the distribution name (the name used with pip and PyPI) do not always match. We want to report which distribution caused the command to be installed, so we need to look up the values. """ global _dists_by_mods if _dists_by_mods is None: results = {} for dist in pkg_resources.working_set: try: mod_names = dist.get_metadata('top_level.txt').strip() except Exception: # Could not retrieve metadata. Either the file is not # present or we cannot read it. Ignore the # distribution. pass else: # Distributions may include multiple top-level # packages (see setuptools for an example). for mod_name in mod_names.splitlines(): results[mod_name] = dist.project_name _dists_by_mods = results return _dists_by_mods def _get_distribution_for_module(module): "Return the distribution containing the module." dist_name = None if module: pkg_name = module.__name__.partition('.')[0] dist_name = _get_distributions_by_modules().get(pkg_name) return dist_name @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.""" # replace a None in self._epilog with an empty string parts = [self._epilog or ''] hook_epilogs = filter( None, (h.obj.get_epilog() for h in self._hooks), ) parts.extend(hook_epilogs) app_dist_name = getattr( self, 'app_dist_name', _get_distribution_for_module( inspect.getmodule(self.app) ) ) dist_name = _get_distribution_for_module(inspect.getmodule(self)) if dist_name and dist_name != app_dist_name: parts.append( 'This command is provided by the %s plugin.' % (dist_name,) ) return '\n\n'.join(parts) 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=_argparse.SmartHelpFormatter, conflict_handler='ignore', ) 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. """ parsed_args = self._run_before_hooks(parsed_args) return_code = self.take_action(parsed_args) or 0 return_code = 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 overridden by developers creating new command base classes and only if it is necessary to have different hook processing behavior. """ for hook in self._hooks: ret = hook.obj.before(parsed_args) # If the return is None do not change parsed_args, otherwise # set up to pass it to the next hook if ret is not None: parsed_args = ret return 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 overridden by developers creating new command base classes and only if it is necessary to have different hook processing behavior. """ for hook in self._hooks: ret = hook.obj.after(parsed_args, return_code) # If the return is None do not change return_code, otherwise # set up to pass it to the next hook if ret is not None: return_code = ret return return_code cliff-3.1.0/cliff/_argparse.py0000664000175000017500000001017513637354530016235 0ustar zuulzuul00000000000000# 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. """Overrides of standard argparse behavior.""" from __future__ import absolute_import import argparse import sys import warnings class _ArgumentContainerMixIn(object): # NOTE(dhellmann): We have to override the methods for creating # groups to return our objects that know how to deal with the # special conflict handler. def add_argument_group(self, *args, **kwargs): group = _ArgumentGroup(self, *args, **kwargs) self._action_groups.append(group) return group def add_mutually_exclusive_group(self, **kwargs): group = _MutuallyExclusiveGroup(self, **kwargs) self._mutually_exclusive_groups.append(group) return group def _handle_conflict_ignore(self, action, conflicting_actions): _handle_conflict_ignore( self, self._option_string_actions, action, conflicting_actions, ) class ArgumentParser(_ArgumentContainerMixIn, argparse.ArgumentParser): if sys.version_info < (3, 5): 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 () def _handle_conflict_ignore(container, option_string_actions, new_action, conflicting_actions): # Remember the option strings the new action starts with so we can # restore them as part of error reporting if we need to. original_option_strings = new_action.option_strings # Remove all of the conflicting option strings from the new action # and report an error if none are left at the end. for option_string, action in conflicting_actions: # remove the conflicting option from the new action new_action.option_strings.remove(option_string) warnings.warn( ('Ignoring option string {} for new action ' 'because it conflicts with an existing option.').format( option_string)) # if the option now has no option string, remove it from the # container holding it if not new_action.option_strings: new_action.option_strings = original_option_strings raise argparse.ArgumentError( new_action, ('Cannot resolve conflicting option string, ' 'all names conflict.'), ) class _ArgumentGroup(_ArgumentContainerMixIn, argparse._ArgumentGroup): pass class _MutuallyExclusiveGroup(_ArgumentContainerMixIn, argparse._MutuallyExclusiveGroup): pass 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 cliff-3.1.0/cliff/help.py0000664000175000017500000001060613637354530015221 0ustar zuulzuul00000000000000# 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 from . import utils 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') dists_by_module = command._get_distributions_by_modules() def dist_for_obj(obj): name = inspect.getmodule(obj).__name__.partition('.')[0] return dists_by_module.get(name) app_dist = dist_for_obj(app) 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 utils.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] dist_name = dist_for_obj(factory) if dist_name and dist_name != app_dist: dist_info = ' (' + dist_name + ')' else: dist_info = '' app.stdout.write(' %-13s %s%s\n' % (name, one_liner, dist_info)) 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 utils.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 cliff-3.1.0/cliff/interactive.py0000664000175000017500000001621113637354530016604 0ustar zuulzuul00000000000000# 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: https://cmd2.readthedocs.io/en/latest/ .. _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 ):" app_cmd_header = "Application commands (type help ):" def __init__(self, parent_app, command_manager, stdin, stdout, errexit=False): 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 self.errexit = errexit cmd2.Cmd.__init__(self, 'tab', stdin=stdin, stdout=stdout) def _split_line(self, line): try: return shlex.split(line.parsed.raw) except AttributeError: # cmd2 >= 0.9.1 gives us a Statement not a PyParsing parse # result. parts = shlex.split(line) if getattr(line, 'command', None): parts.insert(0, line.command) return parts 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 = self._split_line(line) ret = self.parent_app.run_subcommand(line_parts) if self.errexit: # Only provide this if errexit is enabled, # otherise keep old behaviour return ret 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. try: # NOTE(coreycb): This try path can be removed once # requirements.txt has cmd2 >= 0.7.3. parsed = self.parsed except AttributeError: try: parsed = self.parser_manager.parsed except AttributeError: # cmd2 >= 0.9.1 does not have a parser manager parsed = lambda x: x # noqa self.default(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 # Create exit alias to quit the interactive shell. do_exit = cmd2.Cmd.do_quit 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 = self._split_line(statement) 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: if hasattr(statement, 'parsed'): # Older cmd2 uses PyParsing statement.parsed.command = cmd_name statement.parsed.args = ' '.join(sub_argv) else: # cmd2 >= 0.9.1 uses shlex and gives us a Statement. statement.command = cmd_name statement.argv = [cmd_name] + sub_argv statement.args = ' '.join(statement.argv) return statement def cmdloop(self): # We don't want the cmd2 cmdloop() behaviour, just call the old one # directly. In part this is because cmd2.cmdloop() doe not return # anything useful and we want to have a useful exit code. return self._cmdloop() cliff-3.1.0/cliff/tests/0000775000175000017500000000000013637354630015057 5ustar zuulzuul00000000000000cliff-3.1.0/cliff/tests/test_sphinxext.py0000664000175000017500000002040613637354530020523 0ustar zuulzuul00000000000000# 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 .. 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 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 .. 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='', nargs='+', help='a user name') output = '\n'.join(sphinxext._format_parser(parser)) self.assertEqual(textwrap.dedent(""" .. program:: hello-world .. code-block:: shell hello-world [ ...] .. 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') parser.add_argument('--required-arg', dest='stuff', required=True, help='a required argument') 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] --required-arg STUFF (--polite | --profane) name .. option:: --language greeting language .. option:: --translate translate to local language .. option:: --write-to-var-log-something-or-other a long opt to force wrapping .. option:: --required-arg a required argument .. option:: --polite use a polite greeting .. option:: --profane use a less polite greeting .. option:: name user name """).lstrip(), output) def test_various_option_names_with_hyphen(self): """Handle options whose name and/or metavar contain hyphen(s)""" parser = argparse.ArgumentParser(prog='hello-world', add_help=False) parser.add_argument('--foo-bar', metavar='', help='foo bar', required=True) parser.add_argument('--foo-bar-baz', metavar='', help='foo bar baz', required=True) parser.add_argument('--foo', metavar='', help='foo', required=True) parser.add_argument('--alpha', metavar='', help='alpha') parser.add_argument('--alpha-beta', metavar='', help='alpha beta') parser.add_argument('--alpha-beta-gamma', metavar='', help='alpha beta gamma') output = '\n'.join(sphinxext._format_parser(parser)) self.assertEqual(textwrap.dedent(""" .. program:: hello-world .. code-block:: shell hello-world --foo-bar --foo-bar-baz --foo [--alpha ] [--alpha-beta ] [--alpha-beta-gamma ] .. option:: --foo-bar foo bar .. option:: --foo-bar-baz foo bar baz .. option:: --foo foo .. option:: --alpha alpha .. option:: --alpha-beta alpha beta .. option:: --alpha-beta-gamma alpha beta gamma """).lstrip(), output) cliff-3.1.0/cliff/tests/utils.py0000664000175000017500000000255713637354530016601 0ustar zuulzuul00000000000000# 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) cliff-3.1.0/cliff/tests/test_formatters_value.py0000664000175000017500000000425713637354530022061 0ustar zuulzuul00000000000000#!/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) cliff-3.1.0/cliff/tests/base.py0000664000175000017500000000214113637354530016340 0ustar zuulzuul00000000000000# -*- 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)) cliff-3.1.0/cliff/tests/test_lister.py0000664000175000017500000001074313637354530017776 0ustar zuulzuul00000000000000#!/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'), ('c', 'A')], ) class ExerciseListerCustomSort(ExerciseLister): need_sort_by_cliff = False 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' parsed_args.sort_columns = [] 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'], ['c', 'A']], 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' parsed_args.sort_columns = [] 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, ) def test_sort_by_column_cliff_side_procedure(self): test_lister = ExerciseLister(mock.Mock(), []) parsed_args = mock.Mock() parsed_args.columns = ('Col1', 'Col2') parsed_args.formatter = 'test' parsed_args.sort_columns = ['Col2', 'Col1'] test_lister.run(parsed_args) f = test_lister._formatter_plugins['test'] args = f.args[0] data = list(args[1]) self.assertEqual([['a', 'A'], ['c', 'A'], ['b', 'B']], data) def test_sort_by_column_data_already_sorted(self): test_lister = ExerciseListerCustomSort(mock.Mock(), []) parsed_args = mock.Mock() parsed_args.columns = ('Col1', 'Col2') parsed_args.formatter = 'test' parsed_args.sort_columns = ['Col2', 'Col1'] test_lister.run(parsed_args) f = test_lister._formatter_plugins['test'] args = f.args[0] data = list(args[1]) self.assertEqual([['a', 'A'], ['b', 'B'], ['c', 'A']], data) def test_sort_by_non_displayed_column(self): test_lister = ExerciseLister(mock.Mock(), []) parsed_args = mock.Mock() parsed_args.columns = ('Col1',) parsed_args.formatter = 'test' parsed_args.sort_columns = ['Col2'] with mock.patch.object(test_lister, 'take_action') as mock_take_action: mock_take_action.return_value = ( ('Col1', 'Col2'), [['a', 'A'], ['b', 'B'], ['c', 'A']] ) test_lister.run(parsed_args) f = test_lister._formatter_plugins['test'] args = f.args[0] data = list(args[1]) self.assertEqual([['a'], ['c'], ['b']], data) def test_sort_by_non_existing_column(self): test_lister = ExerciseLister(mock.Mock(), []) parsed_args = mock.Mock() parsed_args.columns = ('Col1', 'Col2') parsed_args.formatter = 'test' parsed_args.sort_columns = ['no_exist_column'] test_lister.run(parsed_args) f = test_lister._formatter_plugins['test'] args = f.args[0] data = list(args[1]) self.assertEqual([['a', 'A'], ['b', 'B'], ['c', 'A']], data) cliff-3.1.0/cliff/tests/test_command.py0000664000175000017500000001457713637354530020123 0ustar zuulzuul00000000000000# 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 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.", ) parser.add_argument( '-z', dest='zippy', default='zippy-default', help='defined in TestCommand and used in TestArgumentParser', ) 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()) class TestArgumentParser(base.TestBase): def test_option_name_collision(self): cmd = TestCommand(None, None) parser = cmd.get_parser('NAME') # We should have an exception registering an option with a # name that already exists because we configure the argument # parser to ignore conflicts but this option has no other name # to be used. self.assertRaises( argparse.ArgumentError, parser.add_argument, '-z', ) def test_option_name_collision_with_alias(self): cmd = TestCommand(None, None) parser = cmd.get_parser('NAME') # We not should have an exception registering an option with a # name that already exists because we configure the argument # parser to ignore conflicts and this option can be added as # --zero even if the -z is ignored. parser.add_argument('-z', '--zero') def test_resolve_option_with_name_collision(self): cmd = TestCommand(None, None) parser = cmd.get_parser('NAME') parser.add_argument( '-z', '--zero', dest='zero', default='zero-default', ) args = parser.parse_args(['-z', 'foo', 'a', 'b']) self.assertEqual(args.zippy, 'foo') self.assertEqual(args.zero, 'zero-default') cliff-3.1.0/cliff/tests/test_formatters_yaml.py0000664000175000017500000000576213637354530021711 0ustar zuulzuul00000000000000#!/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) cliff-3.1.0/cliff/tests/test_app.py0000664000175000017500000005135013637354530017253 0ustar zuulzuul00000000000000# -*- 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.assertIsNone(app.interpreter) ret = app.run([]) self.assertIsNotNone(app.interpreter) cmdloop = app.interactive_app_factory.return_value.cmdloop cmdloop.assert_called_once_with() self.assertNotEqual(ret, 0) def test_interactive_mode_cmdloop_error(self): app, command = make_app() cmdloop_mock = mock.MagicMock( name='cmdloop', ) cmdloop_mock.return_value = 1 app.interactive_app_factory = mock.MagicMock( name='interactive_app_factory' ) self.assertIsNone(app.interpreter) ret = app.run([]) self.assertIsNotNone(app.interpreter) cmdloop = app.interactive_app_factory.return_value.cmdloop cmdloop.assert_called_once_with() self.assertNotEqual(ret, 0) 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') ret = app.run(['mock']) app.clean_up.assert_called_once_with(command.return_value, 0, None) self.assertEqual(ret, 0) def test_clean_up_error(self): app, command = make_app() app.clean_up = mock.MagicMock(name='clean_up') ret = app.run(['error']) self.assertNotEqual(ret, 0) 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') ret = app.run(['--debug', 'error']) self.assertNotEqual(ret, 0) 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: ret = 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.assertNotEqual(ret, 0) 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) def test_writer_encoding(self): # The word "test" with the e replaced by # Unicode latin small letter e with acute, # U+00E9, utf-8 encoded as 0xC3 0xA9 text = u't\u00E9st' text_utf8 = text.encode('utf-8') if six.PY2: # In PY2 StreamWriter can't accept non-ASCII encoded characters # because it must first promote the encoded byte stream to # unicode in order to encode it in the desired encoding. # Because the encoding of the byte stream is not known at this # point the default-encoding of ASCII is utilized, but you can't # decode a non-ASCII charcater to ASCII. io = six.StringIO() writer = codecs.getwriter('utf-8')(io) self.assertRaises(UnicodeDecodeError, writer.write, text_utf8) # In PY2 with our override of codecs.getwriter we do not # attempt to encode bytes in a str object (only unicode # objects) therefore the final output string should be the # utf-8 encoded byte sequence io = six.StringIO() writer = utils.getwriter('utf-8')(io) writer.write(text) output = io.getvalue() self.assertEqual(text_utf8, output) io = six.StringIO() writer = utils.getwriter('utf-8')(io) writer.write(text_utf8) output = io.getvalue() self.assertEqual(text_utf8, output) else: # In PY3 you can't write encoded bytes to a text writer # instead text functions require text. io = six.StringIO() writer = utils.getwriter('utf-8')(io) self.assertRaises(TypeError, writer.write, text) io = six.StringIO() writer = utils.getwriter('utf-8')(io) self.assertRaises(TypeError, writer.write, text_utf8) cliff-3.1.0/cliff/tests/test_show.py0000664000175000017500000000471513637354530017456 0ustar zuulzuul00000000000000#!/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, ) cliff-3.1.0/cliff/tests/test_command_hooks.py0000664000175000017500000003307413637354530021317 0ustar zuulzuul00000000000000# 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 TestChangeHook(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 parsed_args.added_by_hook = 'othervalue' parsed_args.added_by_before = True return parsed_args def after(self, parsed_args, return_code): self._after_called = True return 24 class TestDisplayChangeHook(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 parsed_args.added_by_hook = 'othervalue' parsed_args.added_by_before = True return parsed_args def after(self, parsed_args, return_code): self._after_called = True return (('Name',), ('othervalue',)) class TestListerChangeHook(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 parsed_args.added_by_hook = 'othervalue' parsed_args.added_by_before = True return parsed_args def after(self, parsed_args, return_code): self._after_called = True return (('Name',), [('othervalue',)]) 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) result = self.cmd.run(None) self.assertTrue(self.hook._after_called) self.assertEqual(result, 42) class TestChangeHooks(base.TestBase): def setUp(self): super(TestChangeHooks, self).setUp() self.app = make_app() self.cmd = TestCommand(self.app, None, cmd_name='test') self.hook = TestChangeHook(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) self.assertEqual(results.added_by_hook, 'othervalue') self.assertTrue(results.added_by_before) def test_after(self): self.assertFalse(self.hook._after_called) parser = self.cmd.get_parser('test') results = parser.parse_args(['--added-by-hook', 'value']) result = self.cmd.run(results) self.assertTrue(self.hook._after_called) self.assertEqual(result, 24) 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 TestShowOneChangeHooks(base.TestBase): def setUp(self): super(TestShowOneChangeHooks, self).setUp() self.app = make_app() self.cmd = TestShowCommand(self.app, None, cmd_name='test') self.hook = TestDisplayChangeHook(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) self.assertEqual(results.added_by_hook, 'othervalue') self.assertTrue(results.added_by_before) def test_after(self): self.assertFalse(self.hook._after_called) parser = self.cmd.get_parser('test') results = parser.parse_args(['--added-by-hook', 'value']) result = self.cmd.run(results) self.assertTrue(self.hook._after_called) self.assertEqual(result, 0) 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) class TestListerChangeHooks(base.TestBase): def setUp(self): super(TestListerChangeHooks, self).setUp() self.app = make_app() self.cmd = TestListerCommand(self.app, None, cmd_name='test') self.hook = TestListerChangeHook(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) self.assertEqual(results.added_by_hook, 'othervalue') self.assertTrue(results.added_by_before) def test_after(self): self.assertFalse(self.hook._after_called) parser = self.cmd.get_parser('test') results = parser.parse_args(['--added-by-hook', 'value']) result = self.cmd.run(results) self.assertTrue(self.hook._after_called) self.assertEqual(result, 0) cliff-3.1.0/cliff/tests/test_interactive.py0000664000175000017500000000672113637354530021012 0ustar zuulzuul00000000000000# -*- 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, errexit, *command_names): fake_command_manager = [(x, None) for x in command_names] return InteractiveApp(FakeApp, fake_command_manager, stdin=None, stdout=None, errexit=errexit) def _test_completenames(self, expecteds, prefix): app = self.make_interactive_app(False, '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(False, *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) def test_no_errexit(self): command_names = set(['show file', 'show folder', 'list all']) app = self.make_interactive_app(False, *command_names) self.assertFalse(app.errexit) def test_errexit(self): command_names = set(['show file', 'show folder', 'list all']) app = self.make_interactive_app(True, *command_names) self.assertTrue(app.errexit) cliff-3.1.0/cliff/tests/test_formatters_table.py0000664000175000017500000005755713637354530022047 0ustar zuulzuul00000000000000#!/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 width = tw.return_value = 80 self.assertEqual( self._expected_mv[width], _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 width = tw.return_value = 50 actual = _table_tester_helper(self._col_names, self._col_data, extra_args=['--fit-width']) self.assertEqual(self._expected_mv[width], actual) self.assertEqual(width, len(actual.splitlines()[0])) @mock.patch('cliff.utils.terminal_width') def test_max_width_45(self, tw): # resize 2 columns width = tw.return_value = 45 actual = _table_tester_helper(self._col_names, self._col_data, extra_args=['--fit-width']) self.assertEqual(self._expected_mv[width], actual) self.assertEqual(width, len(actual.splitlines()[0])) @mock.patch('cliff.utils.terminal_width') def test_max_width_40(self, tw): # resize all columns width = tw.return_value = 40 actual = _table_tester_helper(self._col_names, self._col_data, extra_args=['--fit-width']) self.assertEqual(self._expected_mv[width], actual) self.assertEqual(width, 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 width = tw.return_value = 10 actual = _table_tester_helper(self._col_names, self._col_data, extra_args=['--fit-width']) self.assertEqual(self._expected_mv[width], 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))) cliff-3.1.0/cliff/tests/test_formatters_json.py0000664000175000017500000000760613637354530021717 0ustar zuulzuul00000000000000#!/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) cliff-3.1.0/cliff/tests/test_help.py0000664000175000017500000001537613637354530017433 0ustar zuulzuul00000000000000# 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 ImportError: 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) cliff-3.1.0/cliff/tests/test_complete.py0000664000175000017500000001532213637354530020302 0ustar zuulzuul00000000000000# 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]) cliff-3.1.0/cliff/tests/test_columns.py0000664000175000017500000000222313637354530020146 0ustar zuulzuul00000000000000# 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(), ) cliff-3.1.0/cliff/tests/__init__.py0000664000175000017500000000000013637354530017155 0ustar zuulzuul00000000000000cliff-3.1.0/cliff/tests/test_formatters_csv.py0000664000175000017500000000573113637354530021536 0ustar zuulzuul00000000000000#!/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) cliff-3.1.0/cliff/tests/test_formatters_shell.py0000664000175000017500000000645413637354530022055 0ustar zuulzuul00000000000000#!/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) cliff-3.1.0/cliff/tests/test_utils.py0000664000175000017500000000542513637354530017635 0ustar zuulzuul00000000000000#!/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) cliff-3.1.0/cliff/tests/test_commandmanager.py0000664000175000017500000001777013637354530021454 0ustar zuulzuul00000000000000# 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 command 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) class FauxCommand(command.Command): def take_action(self, parsed_args): return 0 class FauxCommand2(FauxCommand): pass class TestLegacyCommand(base.TestBase): def test_find_legacy(self): mgr = utils.TestCommandManager(None) mgr.add_command('new name', FauxCommand) mgr.add_legacy_command('old name', 'new name') cmd, name, remaining = mgr.find_command(['old', 'name']) self.assertIs(cmd, FauxCommand) self.assertEqual(name, 'old name') def test_legacy_overrides_new(self): mgr = utils.TestCommandManager(None) mgr.add_command('cmd1', FauxCommand) mgr.add_command('cmd2', FauxCommand2) mgr.add_legacy_command('cmd2', 'cmd1') cmd, name, remaining = mgr.find_command(['cmd2']) self.assertIs(cmd, FauxCommand) self.assertEqual(name, 'cmd2') def test_no_legacy(self): mgr = utils.TestCommandManager(None) mgr.add_command('cmd1', FauxCommand) self.assertRaises( ValueError, mgr.find_command, ['cmd2'], ) def test_no_command(self): mgr = utils.TestCommandManager(None) mgr.add_legacy_command('cmd2', 'cmd1') self.assertRaises( ValueError, mgr.find_command, ['cmd2'], ) class TestLookupAndFindPartialName(base.TestBase): scenarios = [ ('one-word', {'argv': ['o']}), ('two-words', {'argv': ['t', 'w']}), ('three-words', {'argv': ['t', 'w', 'c']}), ] 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 TestGetByPartialName(base.TestBase): def setUp(self): super(TestGetByPartialName, self).setUp() self.commands = { 'resource provider list': 1, 'resource class list': 2, 'server list': 3, 'service list': 4} def test_no_candidates(self): self.assertEqual( [], commandmanager._get_commands_by_partial_name( ['r', 'p'], self.commands)) self.assertEqual( [], commandmanager._get_commands_by_partial_name( ['r', 'p', 'c'], self.commands)) def test_multiple_candidates(self): self.assertEqual( 2, len(commandmanager._get_commands_by_partial_name( ['se', 'li'], self.commands))) def test_one_candidate(self): self.assertEqual( ['resource provider list'], commandmanager._get_commands_by_partial_name( ['r', 'p', 'l'], self.commands)) self.assertEqual( ['resource provider list'], commandmanager._get_commands_by_partial_name( ['resource', 'provider', 'list'], self.commands)) self.assertEqual( ['server list'], commandmanager._get_commands_by_partial_name( ['serve', 'l'], self.commands)) cliff-3.1.0/cliff/tests/test__argparse.py0000664000175000017500000000373313637354530020440 0ustar zuulzuul00000000000000#!/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 unittest from cliff import _argparse class TestArgparse(unittest.TestCase): def test_argument_parser(self): _argparse.ArgumentParser(conflict_handler='ignore') def test_argument_parser_add_group(self): parser = _argparse.ArgumentParser(conflict_handler='ignore') parser.add_argument_group() def test_argument_parser_add_mutually_exclusive_group(self): parser = _argparse.ArgumentParser(conflict_handler='ignore') parser.add_mutually_exclusive_group() def test_argument_parser_add_nested_group(self): parser = _argparse.ArgumentParser(conflict_handler='ignore') group = parser.add_argument_group() group.add_argument_group() def test_argument_parser_add_nested_mutually_exclusive_group(self): parser = _argparse.ArgumentParser(conflict_handler='ignore') group = parser.add_argument_group() group.add_mutually_exclusive_group() def test_argument_parser_add_mx_nested_group(self): parser = _argparse.ArgumentParser(conflict_handler='ignore') group = parser.add_mutually_exclusive_group() group.add_argument_group() def test_argument_parser_add_mx_nested_mutually_exclusive_group(self): parser = _argparse.ArgumentParser(conflict_handler='ignore') group = parser.add_mutually_exclusive_group() group.add_mutually_exclusive_group() cliff-3.1.0/cliff/show.py0000664000175000017500000000356513637354530015257 0ustar zuulzuul00000000000000# 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())) cliff-3.1.0/cliff/complete.py0000664000175000017500000001464613637354530016111 0ustar zuulzuul00000000000000# 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='', help="Command name to support with command completion" ) parser.add_argument( "--shell", default='bash', metavar='', 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 cliff-3.1.0/cliff/display.py0000664000175000017500000001133613637354530015737 0ustar zuulzuul00000000000000# 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', ) self._formatter_group = formatter_group 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 to show multiple columns', ) 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. ' 'Recognized columns are %s.' % (str(parsed_args.columns), str(column_names))) # 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): 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) column_names, data = 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) cliff-3.1.0/cliff/__init__.py0000664000175000017500000000000013637354530016013 0ustar zuulzuul00000000000000cliff-3.1.0/cliff/hooks.py0000664000175000017500000000366613637354530015424 0ustar zuulzuul00000000000000# 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. :param parsed_args: The arguments to the command. :paramtype parsed_args: argparse.Namespace :returns: argparse.Namespace """ return parsed_args @abc.abstractmethod def after(self, parsed_args, return_code): """Called after the command's take_action() method. :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 :returns: int """ return return_code cliff-3.1.0/cliff/commandmanager.py0000664000175000017500000001170113637354530017237 0ustar zuulzuul00000000000000# 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 logging import pkg_resources from . import utils LOG = logging.getLogger(__name__) def _get_commands_by_partial_name(args, commands): n = len(args) candidates = [] for command_name in commands: command_parts = command_name.split() if len(command_parts) != n: continue if all(command_parts[i].startswith(args[i]) for i in range(n)): candidates.append(command_name) return candidates 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._legacy = {} self.namespace = namespace self.convert_underscores = convert_underscores self._load_commands() def _load_commands(self): # NOTE(jamielennox): kept for compatibility. if self.namespace: 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 add_legacy_command(self, old_name, new_name): """Map an old command name to the new name. :param old_name: The old command name. :type old_name: str :param new_name: The new command name. :type new_name: str """ self._legacy[old_name] = new_name 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:] # The legacy command handling may modify name, so remember # the value we actually found in argv so we can return it. return_name = name # Convert the legacy command name to its new name. if name in self._legacy: name = self._legacy[name] found = None if name in self.commands: found = name else: candidates = _get_commands_by_partial_name( argv[:i], self.commands) if len(candidates) == 1: found = candidates[0] if found: cmd_ep = self.commands[found] 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 = utils.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, return_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) cliff-3.1.0/.testr.conf0000664000175000017500000000050513637354530014717 0ustar zuulzuul00000000000000[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 cliff-3.1.0/test-requirements.txt0000664000175000017500000000117113637354530017072 0ustar zuulzuul00000000000000# 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>=1.0.0 # Apache-2.0/BSD testrepository>=0.0.18 # Apache-2.0/BSD testtools>=2.2.0 # MIT mock>=2.0.0 # BSD testscenarios>=0.4 # Apache-2.0/BSD coverage!=4.4,>=4.0 # Apache-2.0 # sphinx is required in test-requirements in addition to doc/requirements # because there is a sphinx extension that has tests sphinx!=1.6.6,!=1.6.7,!=2.1.0,>=1.6.2 # BSD # Bandit security code scanner bandit>=1.1.0 # Apache-2.0 cliff-3.1.0/setup.cfg0000664000175000017500000000342213637354630014454 0ustar zuulzuul00000000000000[metadata] name = cliff description-file = README.rst author = OpenStack author-email = openstack-discuss@lists.openstack.org summary = Command Line Interface Formulation Framework home-page = https://docs.openstack.org/cliff/latest/ python-requires = >=3.5 classifier = Development Status :: 5 - Production/Stable License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython Intended Audience :: Developers Environment :: Console [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 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 [egg_info] tag_build = tag_date = 0 cliff-3.1.0/setup.py0000664000175000017500000000137613637354530014352 0ustar zuulzuul00000000000000# 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 setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) cliff-3.1.0/integration-tests/0000775000175000017500000000000013637354630016315 5ustar zuulzuul00000000000000cliff-3.1.0/integration-tests/openstackclient-tip.sh0000775000175000017500000000112713637354530022634 0ustar zuulzuul00000000000000#!/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 cliff-3.1.0/integration-tests/neutronclient-tip.sh0000775000175000017500000000073513637354530022343 0ustar zuulzuul00000000000000#!/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 cliff-3.1.0/.zuul.yaml0000664000175000017500000000221613637354530014573 0ustar zuulzuul00000000000000- job: name: cliff-tox-py37-neutronclient-tip parent: openstack-tox-py37 description: | Run unit tests for neutronclient with master branch of cliff Uses tox with the ``unit-tips`` environment and master branch of the required-projects below. branches: ^master$ irrelevant-files: - ^.*\.rst$ - ^doc/.*$ - ^releasenotes/.*$ required-projects: - openstack/cliff - openstack/python-neutronclient vars: # Set work dir to neutronclient so that if it's triggered by one of the # other repos the tests will run in the same place zuul_work_dir: src/opendev.org/openstack/python-neutronclient - project: templates: - check-requirements - lib-forward-testing-python3 - openstack-lower-constraints-jobs - openstack-python35-jobs - openstack-python3-ussuri-jobs - publish-openstack-docs-pti check: jobs: - cliff-tox-py37-neutronclient-tip - osc-tox-py36-tips: branches: ^master$ gate: jobs: - cliff-tox-py37-neutronclient-tip - osc-tox-py36-tips: branches: ^master$ cliff-3.1.0/cliff.egg-info/0000775000175000017500000000000013637354630015407 5ustar zuulzuul00000000000000cliff-3.1.0/cliff.egg-info/entry_points.txt0000664000175000017500000000173313637354630020711 0ustar zuulzuul00000000000000[cliff.demo] error = cliffdemo.simple:Error file = cliffdemo.show:File files = cliffdemo.list:Files hooked = cliffdemo.hook:Hooked list files = cliffdemo.list:Files show file = cliffdemo.show:File simple = cliffdemo.simple:Simple two_part = cliffdemo.simple:Simple unicode = cliffdemo.encoding:Encoding [cliff.demo.hooked] sample-hook = cliffdemo.hook:Hook [cliff.formatter.completion] bash = cliff.complete:CompleteBash none = cliff.complete:CompleteNoCode [cliff.formatter.list] csv = cliff.formatters.commaseparated:CSVLister json = cliff.formatters.json_format:JSONFormatter table = cliff.formatters.table:TableFormatter value = cliff.formatters.value:ValueFormatter yaml = cliff.formatters.yaml_format:YAMLFormatter [cliff.formatter.show] json = cliff.formatters.json_format:JSONFormatter shell = cliff.formatters.shell:ShellFormatter table = cliff.formatters.table:TableFormatter value = cliff.formatters.value:ValueFormatter yaml = cliff.formatters.yaml_format:YAMLFormatter cliff-3.1.0/cliff.egg-info/top_level.txt0000664000175000017500000000000613637354630020135 0ustar zuulzuul00000000000000cliff cliff-3.1.0/cliff.egg-info/not-zip-safe0000664000175000017500000000000113637354630017635 0ustar zuulzuul00000000000000 cliff-3.1.0/cliff.egg-info/PKG-INFO0000664000175000017500000000372713637354630016515 0ustar zuulzuul00000000000000Metadata-Version: 1.2 Name: cliff Version: 3.1.0 Summary: Command Line Interface Formulation Framework Home-page: https://docs.openstack.org/cliff/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: UNKNOWN Description: ======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/cliff.svg :target: https://governance.openstack.org/tc/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: https://docs.openstack.org/cliff/latest/ * Source: https://opendev.org/openstack/cliff * Bugs: https://bugs.launchpad.net/python-cliff * Contributors: https://github.com/openstack/cliff/graphs/contributors Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Intended Audience :: Developers Classifier: Environment :: Console Requires-Python: >=3.5 cliff-3.1.0/cliff.egg-info/SOURCES.txt0000664000175000017500000000453413637354630017301 0ustar zuulzuul00000000000000.stestr.conf .testr.conf .zuul.yaml AUTHORS CONTRIBUTING.rst ChangeLog LICENSE README.rst bandit.yaml lower-constraints.txt requirements.txt setup.cfg setup.py test-requirements.txt tox.ini cliff/__init__.py cliff/_argparse.py cliff/app.py cliff/columns.py cliff/command.py cliff/commandmanager.py cliff/complete.py cliff/display.py cliff/help.py cliff/hooks.py cliff/interactive.py cliff/lister.py cliff/show.py cliff/sphinxext.py cliff/utils.py cliff.egg-info/PKG-INFO cliff.egg-info/SOURCES.txt cliff.egg-info/dependency_links.txt cliff.egg-info/entry_points.txt cliff.egg-info/not-zip-safe cliff.egg-info/pbr.json cliff.egg-info/requires.txt cliff.egg-info/top_level.txt cliff/formatters/__init__.py cliff/formatters/base.py cliff/formatters/commaseparated.py cliff/formatters/json_format.py cliff/formatters/shell.py cliff/formatters/table.py cliff/formatters/value.py cliff/formatters/yaml_format.py cliff/tests/__init__.py cliff/tests/base.py cliff/tests/test__argparse.py cliff/tests/test_app.py cliff/tests/test_columns.py cliff/tests/test_command.py cliff/tests/test_command_hooks.py cliff/tests/test_commandmanager.py cliff/tests/test_complete.py cliff/tests/test_formatters_csv.py cliff/tests/test_formatters_json.py cliff/tests/test_formatters_shell.py cliff/tests/test_formatters_table.py cliff/tests/test_formatters_value.py cliff/tests/test_formatters_yaml.py cliff/tests/test_help.py cliff/tests/test_interactive.py cliff/tests/test_lister.py cliff/tests/test_show.py cliff/tests/test_sphinxext.py cliff/tests/test_utils.py cliff/tests/utils.py demoapp/README.rst demoapp/setup.py demoapp/cliffdemo/__init__.py demoapp/cliffdemo/__main__.py demoapp/cliffdemo/encoding.py demoapp/cliffdemo/hook.py demoapp/cliffdemo/list.py demoapp/cliffdemo/main.py demoapp/cliffdemo/show.py demoapp/cliffdemo/simple.py doc/requirements.txt doc/source/conf.py doc/source/index.rst doc/source/contributors/index.rst doc/source/install/index.rst doc/source/reference/index.rst doc/source/user/complete.rst doc/source/user/demoapp.rst doc/source/user/history.rst doc/source/user/index.rst doc/source/user/interactive_mode.rst doc/source/user/introduction.rst doc/source/user/list_commands.rst doc/source/user/show_commands.rst doc/source/user/sphinxext.rst integration-tests/neutronclient-tip.sh integration-tests/openstackclient-tip.sh releasenotes/notes/drop-python27-support-b16c9e5a9e2000ef.yamlcliff-3.1.0/cliff.egg-info/pbr.json0000664000175000017500000000005613637354630017066 0ustar zuulzuul00000000000000{"git_version": "5405c3d", "is_release": true}cliff-3.1.0/cliff.egg-info/dependency_links.txt0000664000175000017500000000000113637354630021455 0ustar zuulzuul00000000000000 cliff-3.1.0/cliff.egg-info/requires.txt0000664000175000017500000000020213637354630020001 0ustar zuulzuul00000000000000pbr!=2.1.0,>=2.0.0 cmd2!=0.8.3,<0.9.0,>=0.8.0 PrettyTable<0.8,>=0.7.2 pyparsing>=2.1.0 six>=1.10.0 stevedore>=1.20.0 PyYAML>=3.12 cliff-3.1.0/lower-constraints.txt0000664000175000017500000000112613637354530017067 0ustar zuulzuul00000000000000alabaster==0.7.10 Babel==2.3.4 bandit==1.1.0 cmd2==0.8.0 coverage==4.0 docutils==0.11 extras==1.0.0 fixtures==3.0.0 gitdb==0.6.4 GitPython==1.0.1 imagesize==0.7.1 Jinja2==2.10 linecache2==1.0.0 MarkupSafe==1.0 mock==2.0.0 pbr==2.0.0 prettytable==0.7.2 Pygments==2.2.0 pyparsing==2.1.0 pyperclip==1.5.27 python-mimeparse==1.6.0 python-subunit==1.0.0 pytz==2013.6 PyYAML==3.12 requests==2.14.2 six==1.10.0 smmap==0.9.0 snowballstemmer==1.2.1 Sphinx==1.6.2 sphinxcontrib-websupport==1.0.1 stevedore==1.20.0 testrepository==0.0.18 testscenarios==0.4 testtools==2.2.0 traceback2==1.4.0 unittest2==1.1.0 cliff-3.1.0/releasenotes/0000775000175000017500000000000013637354630015323 5ustar zuulzuul00000000000000cliff-3.1.0/releasenotes/notes/0000775000175000017500000000000013637354630016453 5ustar zuulzuul00000000000000cliff-3.1.0/releasenotes/notes/drop-python27-support-b16c9e5a9e2000ef.yaml0000664000175000017500000000017713637354530026001 0ustar zuulzuul00000000000000--- upgrade: - | Support for Python 2.7 has been dropped. The minimum version of Python now supported is Python 3.6. cliff-3.1.0/requirements.txt0000664000175000017500000000061413637354530016116 0ustar zuulzuul00000000000000# 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.8.0,!=0.8.3,<0.9.0 # MIT PrettyTable<0.8,>=0.7.2 # BSD pyparsing>=2.1.0 # MIT six>=1.10.0 # MIT stevedore>=1.20.0 # Apache-2.0 PyYAML>=3.12 # MIT cliff-3.1.0/LICENSE0000664000175000017500000002613613637354530013646 0ustar zuulzuul00000000000000 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.