././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708613801.2612169 cliff-4.6.0/0000775000175000017500000000000000000000000012627 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/.pre-commit-config.yaml0000664000175000017500000000155200000000000017113 0ustar00zuulzuul00000000000000--- default_language_version: # force all unspecified python hooks to run python3 python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - id: trailing-whitespace - id: mixed-line-ending args: ['--fix', 'lf'] exclude: '.*\.(svg)$' - id: check-byte-order-marker - id: check-executables-have-shebangs - id: check-merge-conflict - id: debug-statements - id: check-yaml files: .*\.(yaml|yml)$ exclude: '^zuul.d/.*$' - repo: https://github.com/PyCQA/bandit rev: 1.7.5 hooks: - id: bandit args: ['-c', 'bandit.yaml'] # - repo: https://github.com/psf/black # rev: 23.3.0 # hooks: # - id: black # args: ['-S', '-l', '79'] - repo: https://github.com/pycqa/flake8 rev: 6.1.0 hooks: - id: flake8 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/.stestr.conf0000664000175000017500000000004700000000000015101 0ustar00zuulzuul00000000000000[DEFAULT] test_path=./cliff top_dir=./ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/.zuul.yaml0000664000175000017500000000207600000000000014575 0ustar00zuulzuul00000000000000- job: name: cliff-tox-py38-neutronclient-tip parent: openstack-tox-py38 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-python3-jobs - publish-openstack-docs-pti check: jobs: - cliff-tox-py38-neutronclient-tip - osc-tox-py38-tips: branches: ^master$ gate: jobs: - cliff-tox-py38-neutronclient-tip - osc-tox-py38-tips: branches: ^master$ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613801.0 cliff-4.6.0/AUTHORS0000664000175000017500000001052500000000000013702 0ustar00zuulzuul00000000000000Akihiro Motoki Alessandro Pilotti Andrea Frittoli Andreas Jaeger Andreas Jaeger Andrew Spiers Andrey Volkov Atsushi SAKAI Benjamin A. Beasley 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 Felix Yan Feodor Tersin Flavio Percoco Ghanshyam Mann Hervé Beraud Hongbin Lu Ilya Shakhat James Downs James E. Blair Jamie Lennox Jaspreet Singh Rawel Jeremy Stanley Jiri Podivin Joe Server Joel Capitao John Dennis Jonathan LaCour Juan Antonio Osorio Robles Julien Danjou KATO Tomoyuki Ken'ichi Ohmichi Kien Nguyen Louis Sautier 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 OpenStack Release Bot Pavlo Shchelokovskyy Pierre-André MOREY Qiu Yu Rajath Agasthya Ricardo Kirkner Rodolfo Alonso Hernandez Rui Chen Ruslan Aliev Ryan Petrello Ryan Selden Sascha Peilicke Sean Perry Sorin Sbarnea Stephen Finucane Steve Baker Steve Martinelli Takashi Kajinami Terry Howe TerryHowe Thiago Paiva Brito Thomas Bechtold Thomas Herve Tomaz Muraus Tony Breeds Tony Xu Vincent Legoll Vitalii Kulanov Yalei Wang Yandong Xuan Yossi Ovadia Yushiro FURUKAWA Zane Bitter ZhongShengping caoyue dineshbhor gcmalloc gengchc2 heavenshell howardlee kafka kangyufei likui lingyongxu liyingjun ljhuang markmcclain matbu melissaml qingszhao qneill shizhihui songwenping wu.chunyang xuanyandong yanpuqing ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/CONTRIBUTING.rst0000664000175000017500000000056300000000000015274 0ustar00zuulzuul00000000000000Changes 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 Storyboard, not GitHub or Launchpad: https://storyboard.openstack.org/#!/project/openstack/cliff ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613800.0 cliff-4.6.0/ChangeLog0000664000175000017500000006123400000000000014407 0ustar00zuulzuul00000000000000CHANGES ======= 4.6.0 ----- * Test python 3.10 and 3.11 4.5.0 ----- * Use importlib.metadata on Python 3.10+ 4.4.0 ----- * Handle complex objects in yaml formatter better * Add pre-commit * Fix pre-commit issues * Autofit table output if stdout is a tty * Fix flake8 violation E721 * Use upper-constraint in doc generation 4.3.0 ----- * Removing helper functions providing Python < 3.3 compatibility 4.2.0 ----- * Strip trailing periods when getting description * Clarification of the algorithm used 4.1.0 ----- * columns: Useful \_\_str\_\_, \_\_repr\_\_ implementation * Add Python3 antelope unit tests 4.0.0 ----- * Removing brackets around tested conditional * Replace abc.abstractproperty with property and abc.abstractmethod * Remove final use of pkg\_resources * Defer loading PyYAML * Defer loading cmd2 * requirements: Remove explicit python-subunit dependency * requirements: Remove explicit pbr dependency * requirements: Remove explicit pyparsing dependency * Update Python testing per Zed cycle testing runtime * Migrate Python 3.6/7 jobs to Python 3.8 3.10.1 ------ * Removing modindex link from docs 3.10.0 ------ * Add Python3 yoga unit tests 3.9.0 ----- * Automatically page interactive root help output * Colourise and automatically page help output * Update unit test to satisfy python3.10+ * Handle SIGPIPE exit gracefully * Add conflict\_handler parameter as attribut in Command class 3.8.0 ----- * setup.cfg: Replace dashes with underscores * Replace getargspec with getfullargspec * setup.cfg: Replace dashes with underscores * Use py3 as the default runtime for tox * Add Python3 xena unit tests 3.7.0 ----- * requirements: Uncap PrettyTable * Add '--sort-ascending', '--sort-descending' parameters * Make 'FormattableColumn' comparable * Handle null values when sorting * Remove unicode from code * gitignore: Ignore reno artefacts * Remove lower-constraints 3.6.0 ----- * trivial: Remove references to Python 2.7 3.5.0 ----- * columns: Make 'FormattableColumn' comparable * Update requirements URLs in tox config * Remove six * Update requirements * doc: Update bug tracker to storyboard * Remove references to setuptools * Add py38 package metadata * Remove Babel from lower-constraints.txt * Bump py37 to py38 in tox.ini * List setuptools under install\_requires * Document KeyboardInterrupt exit code * Exit gracefully on Ctrl-C * change help action to use its own exception for exit * Add Python3 wallaby unit tests * Capturing argparse errors due to problem with cmd2 3.4.0 ----- * switch to stevedore for loading entry points 3.3.0 ----- * Remove cap on cmd2 * Fix compatibility with new cmd2 3.2.0 ----- * drop mock from lower-constraints and requirements * Import command group support from osc-lib * Remove unneeded tests * Migrate to stestr * Remove python3.5 * Stop to use the \_\_future\_\_ module * Switch to newer openstackdocstheme version * Use unittest.mock instead of third party mock * Add Python3 victoria unit tests 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/LICENSE0000664000175000017500000002613600000000000013644 0ustar00zuulzuul00000000000000 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. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708613801.2612169 cliff-4.6.0/PKG-INFO0000664000175000017500000000376500000000000013737 0ustar00zuulzuul00000000000000Metadata-Version: 1.2 Name: cliff Version: 4.6.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 `entry points`_ to provide subcommands, output formatters, and other extensions. .. _entry points: https://packaging.python.org/specifications/entry-points/ * Free software: Apache license * Documentation: https://docs.openstack.org/cliff/latest/ * Source: https://opendev.org/openstack/cliff * Bugs: https://storyboard.openstack.org/#!/project/openstack/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.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Intended Audience :: Developers Classifier: Environment :: Console Requires-Python: >=3.8 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/README.rst0000664000175000017500000000163000000000000014316 0ustar00zuulzuul00000000000000======================== 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 `entry points`_ to provide subcommands, output formatters, and other extensions. .. _entry points: https://packaging.python.org/specifications/entry-points/ * Free software: Apache license * Documentation: https://docs.openstack.org/cliff/latest/ * Source: https://opendev.org/openstack/cliff * Bugs: https://storyboard.openstack.org/#!/project/openstack/cliff * Contributors: https://github.com/openstack/cliff/graphs/contributors ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/bandit.yaml0000664000175000017500000000005000000000000014747 0ustar00zuulzuul00000000000000skips: - B110 exclude_dirs: - tests ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708613801.2372155 cliff-4.6.0/cliff/0000775000175000017500000000000000000000000013712 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/__init__.py0000664000175000017500000000000000000000000016011 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/_argparse.py0000664000175000017500000000734300000000000016236 0ustar00zuulzuul00000000000000# 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.""" import argparse as orig_argparse import warnings from autopage import argparse 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): pass 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, orig_argparse._ArgumentGroup): pass class _MutuallyExclusiveGroup(_ArgumentContainerMixIn, orig_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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/app.py0000664000175000017500000004052000000000000015045 0ustar00zuulzuul00000000000000# 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 inspect import locale import logging import logging.handlers import os import sys from cliff import _argparse from . import complete from . import help from . import utils logging.getLogger('cliff').addHandler(logging.NullHandler()) # Exit code for exiting due to a signal is 128 + the signal number _SIGINT_EXIT = 130 _SIGPIPE_EXIT = 141 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. 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 BrokenPipeError: return _SIGPIPE_EXIT 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 except KeyboardInterrupt: return _SIGINT_EXIT result = 1 if self.interactive_mode: result = self.interact() else: try: result = self.run_subcommand(remainder) except BrokenPipeError: return _SIGPIPE_EXIT except KeyboardInterrupt: return _SIGINT_EXIT 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 inspect.getfullargspec(cmd_factory.__init__).args: kwargs['cmd_name'] = cmd_name cmd = cmd_factory(self, self.options, **kwargs) result = 1 err = None 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) try: parsed_args = cmd_parser.parse_args(sub_argv) except SystemExit as ex: if self.interactive_mode: # Defer importing cmd2 as it is a slow import import cmd2 raise cmd2.exceptions.Cmd2ArgparseError from ex else: raise ex result = cmd.run(parsed_args) except BrokenPipeError as err1: result = _SIGPIPE_EXIT err = err1 raise except help.HelpExit: result = 0 except Exception as err1: err = err1 if self.options.debug: self.LOG.exception(err) else: self.LOG.error(err) except KeyboardInterrupt as err1: result = _SIGINT_EXIT err = err1 raise finally: 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) del err return result ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/columns.py0000664000175000017500000000311500000000000015744 0ustar00zuulzuul00000000000000# 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 class FormattableColumn(object, metaclass=abc.ABCMeta): def __init__(self, value): self._value = value def __eq__(self, other): return ( self.__class__ == other.__class__ and self._value == other._value ) def __lt__(self, other): return ( self.__class__ == other.__class__ and self._value < other._value ) def __str__(self): return self.human_readable() def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.machine_readable()) @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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/command.py0000664000175000017500000001665000000000000015712 0ustar00zuulzuul00000000000000# 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 try: # Python 3.10 and later from importlib.metadata import packages_distributions except ImportError: # Python 3.9 and older from importlib_metadata import packages_distributions 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: # There can be multiple distribution in the case of namespace packages # so we'll just grab the first one _dists_by_mods = { k: v[0] for k, v in packages_distributions().items() } 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 class Command(object, metaclass=abc.ABCMeta): """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 conflict_handler = 'ignore' _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=self.conflict_handler, ) 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/commandmanager.py0000664000175000017500000001334400000000000017242 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Discover and lookup command plugins. """ import inspect import logging import stevedore 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 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.group_list = [] 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""" self.group_list.append(namespace) for ep in stevedore.ExtensionManager(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.entry_point 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 = inspect.getfullargspec(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) def add_command_group(self, group=None): """Adds another group of command entrypoints""" if group: self.load_commands(group) def get_command_groups(self): """Returns a list of the loaded command groups""" return self.group_list def get_command_names(self, group=None): """Returns a list of commands loaded for the specified group""" group_list = [] if group is not None: for ep in stevedore.ExtensionManager(group): cmd_name = ( ep.name.replace('_', ' ') if self.convert_underscores else ep.name ) group_list.append(cmd_name) return group_list return list(self.commands.keys()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/complete.py0000664000175000017500000001460100000000000016076 0ustar00zuulzuul00000000000000# 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 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, str): 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, str): 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/display.py0000664000175000017500000001134200000000000015732 0ustar00zuulzuul00000000000000# 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 stevedore from . import command class DisplayCommandBase(command.Command, metaclass=abc.ABCMeta): """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() @property @abc.abstractmethod def formatter_namespace(self): "String specifying the namespace to use for loading formatter plugins." @property @abc.abstractmethod 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) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708613801.2412157 cliff-4.6.0/cliff/formatters/0000775000175000017500000000000000000000000016100 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/formatters/__init__.py0000664000175000017500000000000000000000000020177 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/formatters/base.py0000664000175000017500000000505400000000000017370 0ustar00zuulzuul00000000000000# 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 class Formatter(object, metaclass=abc.ABCMeta): @abc.abstractmethod def add_argument_group(self, parser): """Add any options to the argument parser. Should use our own argument group. """ class ListFormatter(Formatter, metaclass=abc.ABCMeta): """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 """ class SingleFormatter(Formatter, metaclass=abc.ABCMeta): """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 """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/formatters/commaseparated.py0000664000175000017500000000341700000000000021444 0ustar00zuulzuul00000000000000# 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 csv import os from .base import ListFormatter from cliff import columns 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='\\', ) writer = csv.writer(stdout, **writer_kwargs) writer.writerow(column_names) for row in data: writer.writerow( [(str(c.machine_readable()) if isinstance(c, columns.FormattableColumn) else c) for c in row] ) return ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/formatters/json_format.py0000664000175000017500000000350700000000000021000 0ustar00zuulzuul00000000000000# 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') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/formatters/shell.py0000664000175000017500000000451100000000000017562 0ustar00zuulzuul00000000000000# 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 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 = (str(value.machine_readable()) if isinstance(value, columns.FormattableColumn) else value) if isinstance(value, str): value = value.replace('"', '\\"') if isinstance(name, str): # 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/formatters/table.py0000664000175000017500000001722500000000000017550 0ustar00zuulzuul00000000000000# 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 os import sys import prettytable 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, str): r = r.replace('\r\n', '\n').replace('\r', ' ') new_row.append(r) return new_row def _do_fit(fit_width): if os.name == 'nt': # NOTE(pas-ha) the isatty is not reliable enough on Windows, # so do not try to auto-fit return fit_width return sys.stdout.isatty() or fit_width class TableFormatter(base.ListFormatter, base.SingleFormatter): ALIGNMENTS = { int: 'r', str: 'l', float: 'r', } 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( 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( 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(x, max_width, min_width=0, fit_width=False): """Set maximum widths for columns of table `x`, with the last column recieving either leftover columns or `min_width`, depending on what offers more space. """ if max_width > 0: term_width = max_width elif not _do_fit(fit_width): # Fitting is disabled return else: term_width = utils.terminal_width() 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 any remaining shrink or min_width field = shrink_fields[-1] x.max_width[field] = max(min_width, shrink_remaining) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/formatters/value.py0000664000175000017500000000257000000000000017572 0ustar00zuulzuul00000000000000# 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 """ 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( str(c.machine_readable() if isinstance(c, columns.FormattableColumn) else c) for c in row) + '\n') return def emit_one(self, column_names, data, stdout, parsed_args): for value in data: stdout.write('%s\n' % str( value.machine_readable() if isinstance(value, columns.FormattableColumn) else value) ) return ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/formatters/yaml_format.py0000664000175000017500000000337500000000000020774 0ustar00zuulzuul00000000000000# 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. """ from . import base from cliff import columns def _yaml_friendly(value): if isinstance(value, columns.FormattableColumn): return value.machine_readable() elif hasattr(value, "toDict"): return value.toDict() elif hasattr(value, "to_dict"): return value.to_dict() else: return value class YAMLFormatter(base.ListFormatter, base.SingleFormatter): def add_argument_group(self, parser): pass def emit_list(self, column_names, data, stdout, parsed_args): # the yaml import is slow, so defer loading until we know we want it import yaml items = [] for item in data: items.append( {n: _yaml_friendly(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): # the yaml import is slow, so defer loading until we know we want it import yaml for key, value in zip(column_names, data): dict_data = {key: _yaml_friendly(value)} yaml.safe_dump(dict_data, stream=stdout, default_flow_style=False) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/help.py0000664000175000017500000001307700000000000015224 0ustar00zuulzuul00000000000000# 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 traceback import autopage.argparse from . import command class HelpExit(SystemExit): """Special exception type to trigger quick exit from the application We subclass from SystemExit to preserve API compatibility for anything that used to catch SystemExit, but use a different class so that cliff's Application can tell the difference between something trying to hard-exit and help saying it's done. """ 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 pager = autopage.argparse.help_pager(app.stdout) color = pager.to_terminal() autopage.argparse.use_color_for_parser(parser, color) with pager as out: parser.print_help(out) title_hl = ('\033[4m', '\033[0m') if color else ('', '') out.write('\n%sCommands%s:\n' % title_hl) 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: out.write('Could not load %r\n' % ep) if namespace.debug: traceback.print_exc(file=out) continue try: kwargs = {} fact_args = inspect.getfullargspec(factory.__init__).args if 'cmd_name' in fact_args: kwargs['cmd_name'] = name cmd = factory(app, None, **kwargs) if cmd.deprecated: continue except Exception as err: out.write('Could not instantiate %r: %s\n' % (ep, err)) if namespace.debug: traceback.print_exc(file=out) continue one_liner = cmd.get_description().split('\n')[0].rstrip('.') dist_name = dist_for_obj(factory) if dist_name and dist_name != app_dist: dist_info = ' (' + dist_name + ')' if color: dist_info = '\033[90m%s\033[39m' % dist_info else: dist_info = '' if color: name = '\033[36m%s\033[39m' % name out.write(' %-13s %s%s\n' % (name, one_liner, dist_info)) raise HelpExit() class HelpCommand(command.Command): """print detailed help for another command """ def get_parser(self, prog_name): parser = super(HelpCommand, self).get_parser(prog_name) parser.add_argument('cmd', nargs='*', help='name of the command', ) return parser def take_action(self, parsed_args): if parsed_args.cmd: try: the_cmd = self.app.command_manager.find_command( parsed_args.cmd, ) cmd_factory, cmd_name, search_args = the_cmd except ValueError: # Did not find an exact match cmd = parsed_args.cmd[0] fuzzy_matches = [k[0] for k in self.app.command_manager if k[0].startswith(cmd) ] if not fuzzy_matches: raise self.app.stdout.write('Command "%s" matches:\n' % cmd) for fm in sorted(fuzzy_matches): self.app.stdout.write(' %s\n' % fm) return self.app_args.cmd = search_args kwargs = {} if 'cmd_name' in inspect.getfullargspec(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) pager = autopage.argparse.help_pager(self.app.stdout) with pager as out: autopage.argparse.use_color_for_parser(cmd_parser, pager.to_terminal()) cmd_parser.print_help(out) else: action = HelpAction(None, None, default=self.app) action(self.app.parser, self.app.options, None, None) return 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/hooks.py0000664000175000017500000000364100000000000015413 0ustar00zuulzuul00000000000000# 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 class CommandHook(object, metaclass=abc.ABCMeta): """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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/interactive.py0000664000175000017500000002037400000000000016607 0ustar00zuulzuul00000000000000# 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 autopage.argparse 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: stdout = self.stdout try: with autopage.argparse.help_pager(stdout) as paged_out: self.stdout = paged_out 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) finally: self.stdout = stdout 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): """Hook method executed just before the command is executed by :meth:`~cmd2.Cmd.onecmd` and after adding it to history. :param statement: subclass of str which also contains the parsed input :return: a potentially modified version of the input Statement object """ # NOTE(mordred): The above docstring is copied in from cmd2 because # current cmd2 has a docstring that sphinx finds if we don't override # it, and it breaks sphinx. # 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 = cmd2.Statement( ' '.join(sub_argv), raw=statement.raw, command=cmd_name, arg_list=sub_argv, multiline_command=statement.multiline_command, terminator=statement.terminator, suffix=statement.suffix, pipe_to=statement.pipe_to, output=statement.output, output_to=statement.output_to, ) 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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/lister.py0000664000175000017500000001105500000000000015570 0ustar00zuulzuul00000000000000# 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 logging from . import display class Lister(display.DisplayCommandBase, metaclass=abc.ABCMeta): """Command base class for providing a list of data as output.""" log = logging.getLogger(__name__) @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): """Run command. 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' ), ) sort_dir_group = group.add_mutually_exclusive_group() sort_dir_group.add_argument( '--sort-ascending', action='store_const', dest='sort_direction', const='asc', help=('sort the column(s) in ascending order'), ) sort_dir_group.add_argument( '--sort-descending', action='store_const', dest='sort_direction', const='desc', help=('sort the column(s) in descending order'), ) 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 ] reverse = parsed_args.sort_direction == 'desc' for index in indexes[::-1]: try: # We need to handle unset values (i.e. None) so we sort on # multiple conditions: the first comparing the results of # an 'is None' type check and the second comparing the # actual value. The second condition will only be checked # if the first returns True, which only happens if the # returns from the 'is None' check on the two values are # the same, i.e. both None or both not-None data = sorted( data, key=lambda k: (k[index] is None, k[index]), reverse=reverse, ) except TypeError: # Simply log and then ignore this; sorting is best effort self.log.warning( "Could not sort on field '%s'; unsortable types", parsed_args.sort_columns[index], ) 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/show.py0000664000175000017500000000354000000000000015246 0ustar00zuulzuul00000000000000# 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 from . import display class ShowOne(display.DisplayCommandBase, metaclass=abc.ABCMeta): """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())) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/sphinxext.py0000664000175000017500000003133000000000000016316 0ustar00zuulzuul00000000000000# 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) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1708613801.249216 cliff-4.6.0/cliff/tests/0000775000175000017500000000000000000000000015054 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/__init__.py0000664000175000017500000000000000000000000017153 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/base.py0000664000175000017500000000214100000000000016336 0ustar00zuulzuul00000000000000# -*- 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)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test__argparse.py0000664000175000017500000000373300000000000020436 0ustar00zuulzuul00000000000000#!/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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_app.py0000664000175000017500000004665200000000000017262 0ustar00zuulzuul00000000000000# -*- 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 io from unittest import mock 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 import sys 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) # Register a command that is interrrupted interrupt_command = mock.Mock(name='interrupt_command', spec=c_cmd.Command) interrupt_command_inst = mock.Mock(spec=c_cmd.Command) interrupt_command_inst.run = mock.Mock( side_effect=KeyboardInterrupt ) interrupt_command.return_value = interrupt_command_inst cmd_mgr.add_command('interrupt', interrupt_command) # Register a command that is interrrupted by a broken pipe pipeclose_command = mock.Mock(name='pipeclose_command', spec=c_cmd.Command) pipeclose_command_inst = mock.Mock(spec=c_cmd.Command) pipeclose_command_inst.run = mock.Mock( side_effect=BrokenPipeError ) pipeclose_command.return_value = pipeclose_command_inst cmd_mgr.add_command('pipe-close', pipeclose_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_interrupt_command(self): app, command = make_app() result = app.run(['interrupt']) self.assertEqual(result, 130) def test_pipeclose_command(self): app, command = make_app() result = app.run(['pipe-close']) self.assertEqual(result, 141) 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_clean_up_interrupt(self): app, command = make_app() app.clean_up = mock.MagicMock(name='clean_up') ret = app.run(['interrupt']) 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, 130, mock.ANY), call_args) args, kwargs = call_args self.assertIsInstance(args[2], KeyboardInterrupt) def test_clean_up_pipeclose(self): app, command = make_app() app.clean_up = mock.MagicMock(name='clean_up') ret = app.run(['pipe-close']) 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, 141, mock.ANY), call_args) args, kwargs = call_args self.assertIsInstance(args[2], BrokenPipeError) 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_interrupted_help(self, deferred_help): app, _ = make_app(deferred_help=deferred_help) with mock.patch('cliff.help.HelpAction.__call__', side_effect=KeyboardInterrupt): result = app.run(['--help']) self.assertEqual(result, 130) def test_interrupted_help(self): self._test_interrupted_help(False) def test_interrupted_deferred_help(self): self._test_interrupted_help(True) def _test_pipeclose_help(self, deferred_help): app, _ = make_app(deferred_help=deferred_help) with mock.patch('cliff.help.HelpAction.__call__', side_effect=BrokenPipeError): app.run(['--help']) def test_pipeclose_help(self): self._test_pipeclose_help(False) def test_pipeclose_deferred_help(self): self._test_pipeclose_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 = io.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() 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 = 't\u00E9st' text_utf8 = text.encode('utf-8') # In PY3 you can't write encoded bytes to a text writer # instead text functions require text. out = io.StringIO() writer = codecs.getwriter('utf-8')(out) self.assertRaises(TypeError, writer.write, text) out = io.StringIO() writer = codecs.getwriter('utf-8')(out) self.assertRaises(TypeError, writer.write, text_utf8) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_columns.py0000664000175000017500000000352000000000000020145 0ustar00zuulzuul00000000000000# 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 'I made this string myself: {}'.format(self._value) class TestColumns(unittest.TestCase): def test_machine_readable(self): c = FauxColumn(['list', 'of', 'values']) self.assertEqual(['list', 'of', 'values'], c.machine_readable()) def test_human_readable(self): c = FauxColumn(['list', 'of', 'values']) self.assertEqual( "I made this string myself: ['list', 'of', 'values']", c.human_readable(), ) def test_str(self): c = FauxColumn(['list', 'of', 'values']) self.assertEqual( "I made this string myself: ['list', 'of', 'values']", str(c), ) def test_repr(self): c = FauxColumn(['list', 'of', 'values']) self.assertEqual( "FauxColumn(['list', 'of', 'values'])", repr(c), ) def test_sorting(self): cols = [ FauxColumn('foo'), FauxColumn('bar'), FauxColumn('baz'), FauxColumn('foo'), ] cols.sort() self.assertEqual( ['bar', 'baz', 'foo', 'foo'], [c.machine_readable() for c in cols], ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_command.py0000664000175000017500000001723500000000000020113 0ustar00zuulzuul00000000000000# 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') def test_with_conflict_handler(self): cmd = TestCommand(None, None) cmd.conflict_handler = 'resolve' parser = cmd.get_parser('NAME') self.assertEqual(parser.conflict_handler, 'resolve') def test_raise_conflict_argument_error(self): cmd = TestCommand(None, None) parser = cmd.get_parser('NAME') parser.add_argument( '-f', '--foo', dest='foo', default='foo', ) self.assertRaises( argparse.ArgumentError, parser.add_argument, '-f', ) def test_resolve_conflict_argument(self): cmd = TestCommand(None, None) cmd.conflict_handler = 'resolve' parser = cmd.get_parser('NAME') parser.add_argument( '-f', '--foo', dest='foo', default='foo', ) parser.add_argument( '-f', '--foo', dest='foo', default='bar', ) args = parser.parse_args(['a', 'b']) self.assertEqual(args.foo, 'bar') def test_wrong_conflict_handler(self): cmd = TestCommand(None, None) cmd.conflict_handler = 'wrong' self.assertRaises( ValueError, cmd.get_parser, 'NAME', ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_command_hooks.py0000664000175000017500000003311200000000000021306 0ustar00zuulzuul00000000000000# 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 from stevedore import extension from unittest import mock 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_commandmanager.py0000664000175000017500000002521200000000000021440 0ustar00zuulzuul00000000000000# 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 testscenarios from unittest import mock 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_get_group_all = mock.Mock(return_value=[testcmd]) with mock.patch('stevedore.ExtensionManager', mock_get_group_all) as mock_manager: mgr = commandmanager.CommandManager('test') mock_manager.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_get_group_all = mock.Mock(return_value=[testcmd]) with mock.patch('stevedore.ExtensionManager', mock_get_group_all) as mock_manager: mgr = commandmanager.CommandManager( 'test', convert_underscores=False, ) mock_manager.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_get_group_all = mock.Mock(return_value=[testcmd]) with mock.patch('stevedore.ExtensionManager', mock_get_group_all) as mock_manager: mgr = commandmanager.CommandManager( 'test', convert_underscores=True, ) mock_manager.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)) class FakeCommand(object): @classmethod def load(cls): return cls def __init__(self): return FAKE_CMD_ONE = FakeCommand FAKE_CMD_TWO = FakeCommand FAKE_CMD_ALPHA = FakeCommand FAKE_CMD_BETA = FakeCommand class FakeCommandManager(commandmanager.CommandManager): commands = {} def load_commands(self, namespace): if namespace == 'test': self.commands['one'] = FAKE_CMD_ONE self.commands['two'] = FAKE_CMD_TWO self.group_list.append(namespace) elif namespace == 'greek': self.commands['alpha'] = FAKE_CMD_ALPHA self.commands['beta'] = FAKE_CMD_BETA self.group_list.append(namespace) class TestCommandManagerGroups(base.TestBase): def test_add_command_group(self): mgr = FakeCommandManager('test') # Make sure add_command() still functions mock_cmd_one = mock.Mock() mgr.add_command('mock', mock_cmd_one) cmd_mock, name, args = mgr.find_command(['mock']) self.assertEqual(mock_cmd_one, cmd_mock) # Find a command added in initialization cmd_one, name, args = mgr.find_command(['one']) self.assertEqual(FAKE_CMD_ONE, cmd_one) # Load another command group mgr.add_command_group('greek') # Find a new command cmd_alpha, name, args = mgr.find_command(['alpha']) self.assertEqual(FAKE_CMD_ALPHA, cmd_alpha) # Ensure that the original commands were not overwritten cmd_two, name, args = mgr.find_command(['two']) self.assertEqual(FAKE_CMD_TWO, cmd_two) def test_get_command_groups(self): mgr = FakeCommandManager('test') # Make sure add_command() still functions mock_cmd_one = mock.Mock() mgr.add_command('mock', mock_cmd_one) cmd_mock, name, args = mgr.find_command(['mock']) self.assertEqual(mock_cmd_one, cmd_mock) # Load another command group mgr.add_command_group('greek') gl = mgr.get_command_groups() self.assertEqual(['test', 'greek'], gl) def test_get_command_names(self): mock_cmd_one = mock.Mock() mock_cmd_one.name = 'one' mock_cmd_two = mock.Mock() mock_cmd_two.name = 'cmd two' mock_get_group_all = mock.Mock( return_value=[mock_cmd_one, mock_cmd_two], ) with mock.patch( 'stevedore.ExtensionManager', mock_get_group_all, ) as mock_manager: mgr = commandmanager.CommandManager('test') mock_manager.assert_called_once_with('test') cmds = mgr.get_command_names('test') self.assertEqual(['one', 'cmd two'], cmds) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_complete.py0000664000175000017500000001534000000000000020300 0ustar00zuulzuul00000000000000# 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 """ from unittest 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]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_formatters_csv.py0000664000175000017500000000562700000000000021540 0ustar00zuulzuul00000000000000#!/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 io import unittest from unittest import mock 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 = io.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 = io.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 = io.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 = ('a', 'b', 'c') d1 = ('A', 'B', 'C') happy = '高兴' d2 = ('D', 'E', happy) data = [d1, d2] expected = 'a,b,c\nA,B,C\nD,E,%s\n' % happy output = io.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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_formatters_json.py0000664000175000017500000000761400000000000021714 0ustar00zuulzuul00000000000000#!/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 io import json from unittest import mock from cliff.formatters import json_format from cliff.tests import base from cliff.tests import test_columns 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 = io.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 = io.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 = io.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 = io.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 = io.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 = io.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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_formatters_shell.py0000664000175000017500000000645100000000000022050 0ustar00zuulzuul00000000000000#!/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 io from unittest import mock from cliff.formatters import shell from cliff.tests import base from cliff.tests import test_columns 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 = io.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 = io.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 = io.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"', str('"esc"')) expected = ('a="True"\nb="False"\nc="100"\n' 'd="\\"esc\\""\ne="\\"esc\\""\n') output = io.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 = io.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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_formatters_table.py0000664000175000017500000005760000000000000022032 0ustar00zuulzuul00000000000000#!/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 from io import StringIO from unittest import mock 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 isinstance(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))) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_formatters_value.py0000664000175000017500000000425400000000000022054 0ustar00zuulzuul00000000000000#!/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. from io import StringIO 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 = 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 = 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 = 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 = StringIO() sf.emit_list(c, data, output, None) actual = output.getvalue() self.assertEqual(expected, actual) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_formatters_yaml.py0000664000175000017500000001077400000000000021706 0ustar00zuulzuul00000000000000#!/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. from io import StringIO import yaml from unittest import mock from cliff.formatters import yaml_format from cliff.tests import base from cliff.tests import test_columns class _toDict: def __init__(self, **kwargs): self._data = kwargs def toDict(self): return self._data class _to_Dict: def __init__(self, **kwargs): self._data = kwargs def to_dict(self): return self._data 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 = 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 = 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 = 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 = StringIO() sf.emit_list(c, d, output, args) actual = yaml.safe_load(output.getvalue()) self.assertEqual(expected, actual) def test_one_custom_object(self): sf = yaml_format.YAMLFormatter() c = ('a', 'b', 'toDict', 'to_dict') d = ('A', 'B', _toDict(spam="ham"), _to_Dict(ham="eggs")) expected = { 'a': 'A', 'b': 'B', 'toDict': {"spam": "ham"}, 'to_dict': {"ham": "eggs"} } output = StringIO() args = mock.Mock() sf.emit_one(c, d, output, args) actual = yaml.safe_load(output.getvalue()) self.assertEqual(expected, actual) def test_list_custom_object(self): sf = yaml_format.YAMLFormatter() c = ('a', 'toDict', 'to_dict') d = ( ('A1', _toDict(B=1), _to_Dict(C=1)), ('A2', _toDict(B=2), _to_Dict(C=2)), ('A3', _toDict(B=3), _to_Dict(C=3)) ) expected = [ {'a': 'A1', 'toDict': {'B': 1}, 'to_dict': {'C': 1}}, {'a': 'A2', 'toDict': {'B': 2}, 'to_dict': {'C': 2}}, {'a': 'A3', 'toDict': {'B': 3}, 'to_dict': {'C': 3}} ] output = 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_help.py0000664000175000017500000001535300000000000017424 0ustar00zuulzuul00000000000000# 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 io import os import sys from unittest 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 = io.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 help.HelpExit: 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 = io.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 help.HelpExit: 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 = io.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 = io.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 help.HelpExit: pass help_text = stdout.getvalue() basecommand = os.path.split(sys.argv[0])[1] self.assertIn('usage: %s [--version]' % basecommand, help_text) self.assertRegex(help_text, 'option(s|al arguments):\n --version') 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 = io.StringIO() app = application.App('testing', '1', utils.TestCommandManager(utils.TEST_NAMESPACE), stdout=stdout) app.NAME = 'test' try: app.run(['--help']) except help.HelpExit: 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 = io.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 help.HelpExit: 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 = io.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 help.HelpExit: 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_interactive.py0000664000175000017500000000672100000000000021010 0ustar00zuulzuul00000000000000# -*- 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_lister.py0000664000175000017500000001505300000000000017773 0ustar00zuulzuul00000000000000#!/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 unittest import mock from cliff import lister from cliff.tests import base 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): data = [('a', 'A'), ('b', 'B'), ('c', 'A')] def _load_formatter_plugins(self): return { 'test': FauxFormatter(), } def take_action(self, parsed_args): return (parsed_args.columns, self.data) class ExerciseListerCustomSort(ExerciseLister): need_sort_by_cliff = False class ExerciseListerNullValues(ExerciseLister): data = ExerciseLister.data + [(None, None)] class ExerciseListerDifferentTypes(ExerciseLister): data = ExerciseLister.data + [(1, 0)] 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_reverse_order(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'] parsed_args.sort_direction = 'desc' test_lister.run(parsed_args) f = test_lister._formatter_plugins['test'] args = f.args[0] data = list(args[1]) self.assertEqual([['b', 'B'], ['c', 'A'], ['a', 'A']], 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_column_with_null(self): test_lister = ExerciseListerNullValues(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'], [None, None]], data) def test_sort_by_column_with_different_types(self): test_lister = ExerciseListerDifferentTypes(mock.Mock(), []) parsed_args = mock.Mock() parsed_args.columns = ('Col1', 'Col2') parsed_args.formatter = 'test' parsed_args.sort_columns = ['Col2', 'Col1'] with mock.patch.object(lister.Lister, 'log') as mock_log: test_lister.run(parsed_args) f = test_lister._formatter_plugins['test'] args = f.args[0] data = list(args[1]) # The output should be unchanged self.assertEqual( [['a', 'A'], ['b', 'B'], ['c', 'A'], [1, 0]], data) # but we should have logged a warning mock_log.warning.assert_has_calls([ mock.call("Could not sort on field '%s'; unsortable types", col) for col in parsed_args.sort_columns ]) 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_show.py0000664000175000017500000000473300000000000017454 0ustar00zuulzuul00000000000000#!/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 unittest import mock from cliff import show from cliff.tests import base 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, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_sphinxext.py0000664000175000017500000002040600000000000020521 0ustar00zuulzuul00000000000000# 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/test_utils.py0000664000175000017500000000247200000000000017632 0ustar00zuulzuul00000000000000#!/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 from unittest import mock from cliff.tests import base from cliff import utils class TestTerminalWidth(base.TestBase): def test(self): width = utils.terminal_width() # Results are specific to the execution environment, so only assert # that no error is raised. if width is not None: self.assertIsInstance(width, int) @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() self.assertEqual(10, width) mock_os.get_terminal_size.side_effect = OSError() width = utils.terminal_width() self.assertIs(None, width) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/tests/utils.py0000664000175000017500000000255700000000000016577 0ustar00zuulzuul00000000000000# 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/cliff/utils.py0000664000175000017500000000626000000000000015430 0ustar00zuulzuul00000000000000# 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 # Each edit operation is assigned different cost, such as: # 'w' means swap operation, the cost is 0; # 's' means substitution operation, the cost is 2; # 'a' means insertion operation, the cost is 1; # 'd' means deletion operation, the cost is 3; # The smaller cost results in the better similarity. COST = {'w': 0, 's': 2, 'a': 1, 'd': 3} def damerau_levenshtein(s1, s2, cost): """Calculates the Damerau-Levenshtein distance between two strings. The Damerau-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/Damerau%E2%80%93Levenshtein_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(): """Return terminal width in columns Uses `os.get_terminal_size` function :returns: terminal width :rtype: int or None """ try: return os.get_terminal_size().columns except OSError: return None ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708613801.2412157 cliff-4.6.0/cliff.egg-info/0000775000175000017500000000000000000000000015404 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613801.0 cliff-4.6.0/cliff.egg-info/PKG-INFO0000664000175000017500000000376500000000000016514 0ustar00zuulzuul00000000000000Metadata-Version: 1.2 Name: cliff Version: 4.6.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 `entry points`_ to provide subcommands, output formatters, and other extensions. .. _entry points: https://packaging.python.org/specifications/entry-points/ * Free software: Apache license * Documentation: https://docs.openstack.org/cliff/latest/ * Source: https://opendev.org/openstack/cliff * Bugs: https://storyboard.openstack.org/#!/project/openstack/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.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Intended Audience :: Developers Classifier: Environment :: Console Requires-Python: >=3.8 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613801.0 cliff-4.6.0/cliff.egg-info/SOURCES.txt0000664000175000017500000000524300000000000017274 0ustar00zuulzuul00000000000000.pre-commit-config.yaml .stestr.conf .zuul.yaml AUTHORS CONTRIBUTING.rst ChangeLog LICENSE README.rst bandit.yaml 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/add-Lister-sort-direction-5f34dba3c9743572.yaml releasenotes/notes/command-group-8c00f260340a130c.yaml releasenotes/notes/comparable-FormattableColumn-31c0030ced70b7fb.yaml releasenotes/notes/drop-python27-support-b16c9e5a9e2000ef.yaml releasenotes/notes/handle-none-values-when-sorting-de40e36c66ad95ca.yaml releasenotes/notes/strip-period-from-help-strings-be368e5cf5bd5269.yaml././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613801.0 cliff-4.6.0/cliff.egg-info/dependency_links.txt0000664000175000017500000000000100000000000021452 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613801.0 cliff-4.6.0/cliff.egg-info/entry_points.txt0000664000175000017500000000173300000000000020706 0ustar00zuulzuul00000000000000[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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613801.0 cliff-4.6.0/cliff.egg-info/not-zip-safe0000664000175000017500000000000100000000000017632 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613801.0 cliff-4.6.0/cliff.egg-info/pbr.json0000664000175000017500000000005600000000000017063 0ustar00zuulzuul00000000000000{"git_version": "b6119d5", "is_release": true}././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613801.0 cliff-4.6.0/cliff.egg-info/requires.txt0000664000175000017500000000020100000000000017775 0ustar00zuulzuul00000000000000PrettyTable>=0.7.2 PyYAML>=3.12 autopage>=0.4.0 cmd2>=1.0.0 stevedore>=2.0.1 [:(python_version<"3.10")] importlib_metadata>=4.4 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613801.0 cliff-4.6.0/cliff.egg-info/top_level.txt0000664000175000017500000000000600000000000020132 0ustar00zuulzuul00000000000000cliff ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1708613801.249216 cliff-4.6.0/demoapp/0000775000175000017500000000000000000000000014254 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/demoapp/README.rst0000664000175000017500000000167700000000000015756 0ustar00zuulzuul00000000000000================= 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 $ ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708613801.2532163 cliff-4.6.0/demoapp/cliffdemo/0000775000175000017500000000000000000000000016204 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/demoapp/cliffdemo/__init__.py0000664000175000017500000000000000000000000020303 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/demoapp/cliffdemo/__main__.py0000664000175000017500000000015100000000000020273 0ustar00zuulzuul00000000000000import sys from cliffdemo.main import main if __name__ == '__main__': sys.exit(main(sys.argv[1:])) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/demoapp/cliffdemo/encoding.py0000664000175000017500000000070700000000000020350 0ustar00zuulzuul00000000000000# -*- 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 = [ 'pi: π', 'GB18030:鼀丅㐀ٸཌྷᠧꌢ€', ] return ( ('UTF-8', 'Unicode'), [(repr(t.encode('utf-8')), t) for t in messages], ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/demoapp/cliffdemo/hook.py0000664000175000017500000000274600000000000017527 0ustar00zuulzuul00000000000000# 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') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/demoapp/cliffdemo/list.py0000664000175000017500000000061200000000000017530 0ustar00zuulzuul00000000000000import 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('.')) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/demoapp/cliffdemo/main.py0000664000175000017500000000153200000000000017503 0ustar00zuulzuul00000000000000import 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:])) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/demoapp/cliffdemo/show.py0000664000175000017500000000145400000000000017542 0ustar00zuulzuul00000000000000import 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/demoapp/cliffdemo/simple.py0000664000175000017500000000104700000000000020051 0ustar00zuulzuul00000000000000import 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') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/demoapp/setup.py0000664000175000017500000000341000000000000015764 0ustar00zuulzuul00000000000000#!/usr/bin/env python from setuptools import find_packages from setuptools import setup PROJECT = 'cliffdemo' # Change docs/sphinx/conf.py too! VERSION = '0.1' 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 :: 3', 'Programming Language :: Python :: 3 :: Only', '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, ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708613801.2532163 cliff-4.6.0/doc/0000775000175000017500000000000000000000000013374 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/doc/requirements.txt0000664000175000017500000000042500000000000016661 0ustar00zuulzuul00000000000000# 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>=2.0.0,!=2.1.0 # BSD openstackdocstheme>=2.2.1 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708613801.2532163 cliff-4.6.0/doc/source/0000775000175000017500000000000000000000000014674 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/doc/source/conf.py0000664000175000017500000002041200000000000016172 0ustar00zuulzuul00000000000000# -*- 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 openstackdocs_repo_name = 'openstack/cliff' openstackdocs_auto_name = False openstackdocs_bug_project = 'python-cliff' openstackdocs_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 = 'cliff' copyright = '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 = 'native' # 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', 'cliff Documentation', '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', 'cliff Documentation', ['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', 'cliff Documentation', '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' ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708613801.2532163 cliff-4.6.0/doc/source/contributors/0000775000175000017500000000000000000000000017431 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/doc/source/contributors/index.rst0000664000175000017500000000311400000000000021271 0ustar00zuulzuul00000000000000================== 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 `Storyboard project`_. .. _Storyboard project: https://storyboard.openstack.org/#!/project/openstack/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 `Storyboard 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. Running Tests ============= The test suite for cliff uses tox_, which must be installed separately (``pip install tox``). To run the standard set of tests, run ``tox`` from the top level directory of the git repository. To run a single environment, specify it using the ``-e`` parameter. For example:: $ tox -e pep8 Add new tests by modifying an existing file or creating new script in the ``tests`` directory. .. _tox: https://tox.readthedocs.io/ Building Documentation ====================== The documentation for cliff is written in reStructuredText and converted to HTML using Sphinx. Like tests, the documentation can be built using ``tox``:: $ tox -e docs The output version of the documentation ends up in ``./docs/build/html``. .. _developer-templates: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/doc/source/index.rst0000664000175000017500000000073500000000000016542 0ustar00zuulzuul00000000000000======================================================= 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:`search` ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708613801.2532163 cliff-4.6.0/doc/source/install/0000775000175000017500000000000000000000000016342 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/doc/source/install/index.rst0000664000175000017500000000135300000000000020205 0ustar00zuulzuul00000000000000============== Installation ============== Python Versions =============== cliff is being developed under Python 3. .. _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_. For example:: $ 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 storyboard: https://storyboard.openstack.org/#!/project/openstack/cliff ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708613801.2532163 cliff-4.6.0/doc/source/reference/0000775000175000017500000000000000000000000016632 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/doc/source/reference/index.rst0000664000175000017500000000163500000000000020500 0ustar00zuulzuul00000000000000======================= 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: ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708613801.2572165 cliff-4.6.0/doc/source/user/0000775000175000017500000000000000000000000015652 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/doc/source/user/complete.rst0000664000175000017500000000230000000000000020207 0ustar00zuulzuul00000000000000==================== 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/doc/source/user/demoapp.rst0000664000175000017500000002152300000000000020034 0ustar00zuulzuul00000000000000======================== 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/doc/source/user/history.rst0000664000175000017500000000004000000000000020077 0ustar00zuulzuul00000000000000.. include:: ../../../ChangeLog ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/doc/source/user/index.rst0000664000175000017500000000044400000000000017515 0ustar00zuulzuul00000000000000============= 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/doc/source/user/interactive_mode.rst0000664000175000017500000000513200000000000021726 0ustar00zuulzuul00000000000000================== 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/doc/source/user/introduction.rst0000664000175000017500000000533500000000000021133 0ustar00zuulzuul00000000000000============== 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 `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. .. _entry points: https://packaging.python.org/specifications/entry-points/ .. _argparse: http://docs.python.org/library/argparse.html ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/doc/source/user/list_commands.rst0000664000175000017500000000703300000000000021243 0ustar00zuulzuul00000000000000=============== 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: https://pypi.org/project/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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/doc/source/user/show_commands.rst0000664000175000017500000000667500000000000021263 0ustar00zuulzuul00000000000000=============== 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: https://pypi.org/project/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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/doc/source/user/sphinxext.rst0000664000175000017500000001572100000000000020444 0ustar00zuulzuul00000000000000==================== Sphinx Integration ==================== Usage ===== cliff supports integration with Sphinx by way of a `Sphinx directives`__. __ http://www.sphinx-doc.org/en/stable/extdev/markupapi.html 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. __ https://docs.python.org/3/library/fnmatch.html `: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' __ https://docs.openstack.org/python-openstackclient/ ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708613801.2572165 cliff-4.6.0/integration-tests/0000775000175000017500000000000000000000000016312 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/integration-tests/neutronclient-tip.sh0000775000175000017500000000073500000000000022341 0ustar00zuulzuul00000000000000#!/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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/integration-tests/openstackclient-tip.sh0000775000175000017500000000112700000000000022632 0ustar00zuulzuul00000000000000#!/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 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1708613801.229215 cliff-4.6.0/releasenotes/0000775000175000017500000000000000000000000015320 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708613801.2612169 cliff-4.6.0/releasenotes/notes/0000775000175000017500000000000000000000000016450 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/releasenotes/notes/add-Lister-sort-direction-5f34dba3c9743572.yaml0000664000175000017500000000162200000000000026431 0ustar00zuulzuul00000000000000--- features: - | The ``cliff.lister.Lister`` base class now implements ``--sort-ascending`` and ``--sort-descending`` options, which can be used to configure the sort direction. For example:: $ hello-world list-users --sort-column email --sort-descending +----------------+-----------------------------+ | Name | Email | +----------------+-----------------------------+ | Charles Xavier | therealcharliex@example.com | | Jim Hendrix | jim@example.com | | John Doe | doe.john@example.com | | Alice Baker | abaker@example.com | +----------------+-----------------------------+ upgrade: - | ``cliff.lister.Lister`` implementations that override the ``need_sort_by_cliff`` property should now consider the ``--sort-ascending`` and ``--sort-descending`` options. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/releasenotes/notes/command-group-8c00f260340a130c.yaml0000664000175000017500000000015700000000000024134 0ustar00zuulzuul00000000000000--- features: - | Added support for command groups. This was originally added in osc_lib downstream. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/releasenotes/notes/comparable-FormattableColumn-31c0030ced70b7fb.yaml0000664000175000017500000000065200000000000027333 0ustar00zuulzuul00000000000000--- features: - | Instances of ``cliff.columns.FormattableColumn`` are now comparable. This allows implementations of ``FormattableColumn`` storing primitive data types or containers with primitive data types to be sorted using the ``--sort-column`` option. Implementations of ``FormattableColumn`` that store other types of data will still need to implement their own rich comparison magic methods. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/releasenotes/notes/drop-python27-support-b16c9e5a9e2000ef.yaml0000664000175000017500000000017700000000000025777 0ustar00zuulzuul00000000000000--- upgrade: - | Support for Python 2.7 has been dropped. The minimum version of Python now supported is Python 3.6. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/releasenotes/notes/handle-none-values-when-sorting-de40e36c66ad95ca.yaml0000664000175000017500000000054100000000000030021 0ustar00zuulzuul00000000000000--- fixes: - | Sorting output using the ``--sort-column`` option will now handle ``None`` values. This was supported implicitly in Python 2 but was broken in the move to Python 3. In addition, requests to sort a column containing non-comparable types will now be ignored. Previously, these request would result in a ``TypeError``. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/releasenotes/notes/strip-period-from-help-strings-be368e5cf5bd5269.yaml0000664000175000017500000000026700000000000027651 0ustar00zuulzuul00000000000000--- features: - | Trailing periods will now be stripped from the oneline summary shown for the help command. This better aligns with the format used by argparse itself. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/requirements.txt0000664000175000017500000000026400000000000016115 0ustar00zuulzuul00000000000000autopage>=0.4.0 # Apache 2.0 importlib_metadata>=4.4;python_version<"3.10" # Apache-2.0 cmd2>=1.0.0 # MIT PrettyTable>=0.7.2 # BSD stevedore>=2.0.1 # Apache-2.0 PyYAML>=3.12 # MIT ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1708613801.2612169 cliff-4.6.0/setup.cfg0000664000175000017500000000347300000000000014457 0ustar00zuulzuul00000000000000[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.8 classifier = Development Status :: 5 - Production/Stable License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/setup.py0000664000175000017500000000144300000000000014343 0ustar00zuulzuul00000000000000# 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'], install_requires=['setuptools'], pbr=True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/test-requirements.txt0000664000175000017500000000051700000000000017073 0ustar00zuulzuul00000000000000stestr>=1.0.0 # Apache-2.0 testtools>=2.2.0 # MIT 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>=2.0.0,!=2.1.0 # BSD # Bandit security code scanner bandit>=1.1.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1708613773.0 cliff-4.6.0/tox.ini0000664000175000017500000000310700000000000014143 0ustar00zuulzuul00000000000000[tox] minversion = 3.1.0 envlist = py3,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 = stestr run {posargs} stestr slowest deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -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 = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W -b html doc/source doc/build/html [testenv:cover] setenv = {[testenv]setenv} PYTHON=coverage run --source cliff --parallel-mode commands = stestr run {posargs} coverage combine coverage html -d cover coverage xml -o cover/coverage.xml